diff --git a/Cargo.toml b/Cargo.toml index 1d86a3e..9462681 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" readme = "README.md" # Own version (decoupled from workspace.version): data-beans evolves its CLI / # QC API independently of the shared utility crates. -version = "0.8.0" +version = "0.8.1" rust-version = "1.91" # Ship only the sources, README and license: nothing else in the working tree # (data stores, saved name lists, scratch files) can end up on crates.io. diff --git a/src/alg/collapse_data/stats.rs b/src/alg/collapse_data/stats.rs index 0967289..032d67e 100644 --- a/src/alg/collapse_data/stats.rs +++ b/src/alg/collapse_data/stats.rs @@ -344,7 +344,7 @@ fn optimize_block( for _opt_iter in 0..num_iter { #[cfg(debug_assertions)] { - debug!("iteration: {}", &_opt_iter); + debug!("iteration: {}", _opt_iter); } update_mu(&mut mu_adj_param, &delta_gb); diff --git a/src/aux/ontology.rs b/src/aux/ontology.rs index 837e6b8..6fcda1c 100644 --- a/src/aux/ontology.rs +++ b/src/aux/ontology.rs @@ -83,10 +83,10 @@ impl Ontology { } TermClause::IsObsolete(b) => obsolete = obsolete || *b, TermClause::IsA(parent) => is_a.push(parent.to_string().trim().into()), - TermClause::Relationship(rel, target) => { - if rel.to_string().trim() == "part_of" { - part_of.push(target.to_string().trim().into()); - } + TermClause::Relationship(rel, target) + if rel.to_string().trim() == "part_of" => + { + part_of.push(target.to_string().trim().into()); } _ => {} } diff --git a/src/handlers/builders/from_fragments.rs b/src/handlers/builders/from_fragments.rs index d36871f..9a22b96 100644 --- a/src/handlers/builders/from_fragments.rs +++ b/src/handlers/builders/from_fragments.rs @@ -131,7 +131,7 @@ pub fn run_build_from_fragments(args: &FromFragmentsArgs) -> anyhow::Result<()> resolve_backend_file(&effective_output, Some(args.backend.clone()))?; if std::path::Path::new(backend_file.as_ref()).exists() { - info!("Removing existing backend file: {}", &backend_file); + info!("Removing existing backend file: {}", backend_file); remove_file(&backend_file)?; } @@ -1110,8 +1110,8 @@ mod tests { /////////////////////////////////////////////////////////////// fn run_pair_and_compare(args_serial: FromFragmentsArgs, args_parallel: FromFragmentsArgs) { - let out_serial = format!("{}.zarr", &args_serial.output); - let out_parallel = format!("{}.zarr", &args_parallel.output); + let out_serial = format!("{}.zarr", args_serial.output); + let out_parallel = format!("{}.zarr", args_parallel.output); run_build_from_fragments(&args_serial).unwrap(); run_build_from_fragments(&args_parallel).unwrap(); let a = open_sparse_matrix(&out_serial, &SparseIoBackend::Zarr).unwrap(); diff --git a/src/handlers/builders/from_mtx.rs b/src/handlers/builders/from_mtx.rs index 2a3220b..291705b 100644 --- a/src/handlers/builders/from_mtx.rs +++ b/src/handlers/builders/from_mtx.rs @@ -92,7 +92,7 @@ pub fn run_build_from_mtx(args: &FromMtxArgs) -> anyhow::Result<()> { if std::path::Path::new(backend_file.as_ref()).exists() { info!( "This existing backend file '{}' will be deleted", - &backend_file + backend_file ); remove_file(&backend_file)?; } diff --git a/src/handlers/merging.rs b/src/handlers/merging.rs index 708fc60..a8fe370 100644 --- a/src/handlers/merging.rs +++ b/src/handlers/merging.rs @@ -342,7 +342,7 @@ pub fn run_merge_backend(args: &MergeBackendArgs) -> anyhow::Result<()> { if std::path::Path::new(backend_file.as_ref()).exists() { info!( "This existing backend file '{}' will be deleted", - &backend_file + backend_file ); remove_file(&backend_file)?; } @@ -398,7 +398,7 @@ pub fn run_merge_backend(args: &MergeBackendArgs) -> anyhow::Result<()> { info!( "Successfully created a sparse backend file: {}", - &backend_file + backend_file ); let batch_map = column_names @@ -407,7 +407,7 @@ pub fn run_merge_backend(args: &MergeBackendArgs) -> anyhow::Result<()> { .collect::>(); if args.do_squeeze { - info!("Squeeze the backend data {}", &backend_file); + info!("Squeeze the backend data {}", backend_file); let squeeze_args = RunSqueezeArgs { // Squeezing in place on a path the caller already resolved: the // zip decision was made there, so nothing is re-zipped here. @@ -459,7 +459,7 @@ pub fn run_merge_mtx(args: &MergeMtxArgs) -> anyhow::Result<()> { if let Some(base) = std::path::Path::new(&dir).file_stem() { let base = base.to_str().expect("invalid base name").to_string(); - info!("Searching relevant files within: {}", &base); + info!("Searching relevant files within: {}", base); let batch_name = Some(base); if let Ok(this_dir) = std::fs::read_dir(&dir) { @@ -482,7 +482,7 @@ pub fn run_merge_mtx(args: &MergeMtxArgs) -> anyhow::Result<()> { } if let (Some(m), Some(r), Some(c), Some(b)) = (mtx, row, col, batch_name) { - info!("Build {} from {}, {}, {} ", &b, &m, &r, &c); + info!("Build {} from {}, {}, {} ", b, m, r, c); mtx_files.push(m); row_files.push(r); col_files.push(c); @@ -491,7 +491,7 @@ pub fn run_merge_mtx(args: &MergeMtxArgs) -> anyhow::Result<()> { } } - info!("Searching subdir within: {}", &dir); + info!("Searching subdir within: {}", dir); let mut sub_dir_vec = std::fs::read_dir(&dir)? .filter_map(Result::ok) @@ -507,7 +507,7 @@ pub fn run_merge_mtx(args: &MergeMtxArgs) -> anyhow::Result<()> { if let Some(base) = std::path::Path::new(&sub_dir).file_stem() { let base = base.to_str().expect("invalid base name").to_string(); - info!("searching {} ...", &base); + info!("searching {} ...", base); let batch_name = Some(base); @@ -516,7 +516,7 @@ pub fn run_merge_mtx(args: &MergeMtxArgs) -> anyhow::Result<()> { if let Some(_path) = x?.path().to_str() { let _path = _path.to_string(); - info!("Found: {}", &_path); + info!("Found: {}", _path); if _path.ends_with(args.mtx_file_name.as_ref()) { mtx = Some(_path.into_boxed_str()); @@ -530,7 +530,7 @@ pub fn run_merge_mtx(args: &MergeMtxArgs) -> anyhow::Result<()> { } if let (Some(m), Some(r), Some(c), Some(b)) = (mtx, row, col, batch_name) { - info!("Build {} from {}, {}, {} ", &b, &m, &r, &c); + info!("Build {} from {}, {}, {} ", b, m, r, c); mtx_files.push(m); row_files.push(r); col_files.push(c); @@ -655,7 +655,7 @@ pub fn run_merge_mtx(args: &MergeMtxArgs) -> anyhow::Result<()> { if std::path::Path::new(&backend_file).exists() { info!( "This existing backend file '{}' will be deleted", - &backend_file + backend_file ); remove_file(&backend_file)?; } @@ -701,7 +701,7 @@ pub fn run_merge_mtx(args: &MergeMtxArgs) -> anyhow::Result<()> { info!( "Successfully created a sparse backend file: {}", - &backend_file + backend_file ); let batch_map = column_names @@ -710,7 +710,7 @@ pub fn run_merge_mtx(args: &MergeMtxArgs) -> anyhow::Result<()> { .collect::>(); if args.do_squeeze { - info!("Squeeze the backend data {}", &backend_file); + info!("Squeeze the backend data {}", backend_file); let squeeze_args = RunSqueezeArgs { // Squeezing in place on a path the caller already resolved: the // zip decision was made there, so nothing is re-zipped here. diff --git a/src/handlers/transformation/squeeze.rs b/src/handlers/transformation/squeeze.rs index af3e3d2..4f125d6 100644 --- a/src/handlers/transformation/squeeze.rs +++ b/src/handlers/transformation/squeeze.rs @@ -273,7 +273,7 @@ fn run_merge_then_squeeze( let (backend, backend_file) = resolve_backend_file(&effective_output, Some(backend))?; if std::path::Path::new(backend_file.as_ref()).exists() { - info!("Removing existing output file: {}", &backend_file); + info!("Removing existing output file: {}", backend_file); remove_file(&backend_file)?; } @@ -289,7 +289,7 @@ fn run_merge_then_squeeze( merged_data.register_column_names_vec(&column_names); merged_data.set_metadata(&meta::common_metadata(&metas))?; - info!("Created merged file: {}", &backend_file); + info!("Created merged file: {}", backend_file); // Step 4: Squeeze the merged result info!( diff --git a/src/handlers/transformation/subset.rs b/src/handlers/transformation/subset.rs index a64b142..9ab4cf1 100644 --- a/src/handlers/transformation/subset.rs +++ b/src/handlers/transformation/subset.rs @@ -168,7 +168,7 @@ pub fn subset_columns(args: &SubsetColumnsArgs) -> anyhow::Result<()> { drop(data); if args.do_squeeze { - info!("Squeeze the backend data {}", &output_file); + info!("Squeeze the backend data {}", output_file); let squeeze_args = build_squeeze_args( output_file.clone(), args.column_nnz_cutoff, @@ -270,7 +270,7 @@ pub fn subset_rows(args: &SubsetRowsArgs) -> anyhow::Result<()> { drop(data); if args.do_squeeze { - info!("Squeeze the backend data {}", &output_file); + info!("Squeeze the backend data {}", output_file); let squeeze_args = build_squeeze_args( output_file.clone(), args.column_nnz_cutoff, diff --git a/src/hdf5_io.rs b/src/hdf5_io.rs index be6b7d6..8de1c91 100644 --- a/src/hdf5_io.rs +++ b/src/hdf5_io.rs @@ -86,8 +86,8 @@ pub fn resolve_backend_file( `hdf5` feature; writing Zarr instead ({} -> {}). Pass \ `--zip=true` (or use a `.zarr.zip` output path) for a zipped \ archive.", - &backend_file, - &new_path + backend_file, + new_path ); return Ok((SparseIoBackend::Zarr, new_path.into_boxed_str())); } diff --git a/src/interactive/ui.rs b/src/interactive/ui.rs index 423a625..b366768 100644 --- a/src/interactive/ui.rs +++ b/src/interactive/ui.rs @@ -3,7 +3,12 @@ //! plot with a y gutter, an x axis, and markers. use ratatui::buffer::Buffer; -use ratatui::crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; +use std::time::{Duration, Instant}; + +use ratatui::crossterm::event::{ + self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, KeyboardEnhancementFlags, + MouseEvent, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, +}; use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Line, Span}; @@ -39,14 +44,87 @@ pub trait Screen { None } fn do_work(&mut self) {} - /// Called about every [`TICK`] while no key comes: true redraws, for a - /// view waiting on work in the background. + /// Called about every [`TICK`], keys or not: true redraws, for a view + /// waiting on work in the background. fn tick(&mut self) -> bool { false } + /// Whether the view takes the mouse: only then is the terminal asked for + /// it, since a view holding it keeps text from being selected. + fn takes_mouse(&self) -> bool { + false + } + /// A mouse event (a button pressed or let go, or a turn of the wheel) + /// at a cell of the screen; whether the screen changed and is drawn + /// again. + fn mouse(&mut self, _event: MouseEvent) -> bool { + false + } + /// Whether the terminal is asked to report keys held with Shift, Alt or + /// Ctrl apart (the kitty keyboard protocol; others ignore the request), + /// so that Shift+Enter, say, is not taken for Enter. + fn reports_chords(&self) -> bool { + false + } + /// Something to tell whoever is away, once: [`run_screen`] rings the + /// terminal's bell and asks it for a desktop notification saying it. + fn take_notice(&mut self) -> Option { + None + } } -/// How long [`run_screen`] waits for a key before asking [`Screen::tick`]. +/// The terminal modes a screen asked for, taken back while its screen is +/// still up: terminals keep each screen's modes apart. +#[derive(Default)] +struct Modes { + mouse: bool, + chords: bool, +} + +/// Buttons and the wheel (1000) in SGR form (1006); not the pointer's every +/// move, which would wake the loop for nothing. +const MOUSE_ON: &str = "\x1b[?1000h\x1b[?1006h"; +const MOUSE_OFF: &str = "\x1b[?1006l\x1b[?1000l"; + +/// Write `text` to the terminal as it is. +fn send(text: &str) -> bool { + let mut out = std::io::stdout(); + std::io::Write::write_all(&mut out, text.as_bytes()).is_ok() + && std::io::Write::flush(&mut out).is_ok() +} + +/// Ring the terminal's bell and ask it for a desktop notification saying +/// `text` (OSC 9); terminals without either ignore them. +pub fn notify(text: &str) { + let text: String = text.chars().filter(|c| !c.is_control()).collect(); + send(&format!("\x07\x1b]9;{text}\x07")); +} + +impl Modes { + fn ask(&mut self, screen: &impl Screen) { + let mut out = std::io::stdout(); + if screen.takes_mouse() && !self.mouse { + self.mouse = send(MOUSE_ON); + } + if screen.reports_chords() && !self.chords { + let flags = KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES; + self.chords = + ratatui::crossterm::execute!(out, PushKeyboardEnhancementFlags(flags)).is_ok(); + } + } + + fn release(&mut self) { + let mut out = std::io::stdout(); + if std::mem::take(&mut self.mouse) { + send(MOUSE_OFF); + } + if std::mem::take(&mut self.chords) { + let _ = ratatui::crossterm::execute!(out, PopKeyboardEnhancementFlags); + } + } +} + +/// How often [`run_screen`] asks [`Screen::tick`], keys or not. pub const TICK: std::time::Duration = std::time::Duration::from_millis(200); /// Holds log records back while alive, writing them when dropped. @@ -69,39 +147,80 @@ impl Drop for HeldLogs { /// return and on panic. Log records raised meanwhile are held back and /// written once the normal screen is back. Blocking work runs on the normal /// screen, where its own progress output belongs, and the view comes back -/// after it. -pub fn run_screen(screen: &mut impl Screen) -> anyhow::Result<()> { +/// after it. The mouse and keys held with modifiers are reported only to a +/// screen that asks for them, and events already queued (a turn of the wheel +/// is several) are taken before the screen is drawn again. +pub fn run_screen(screen: &mut S) -> anyhow::Result<()> { + run_screen_with(screen, S::handle_key) +} + +/// [`run_screen`], with each key (but Ctrl-C) handed to `on_key` rather +/// than [`Screen::handle_key`]: for a caller with rules of its own about +/// which keys a screen sees. +pub fn run_screen_with( + screen: &mut S, + mut on_key: impl FnMut(&mut S, KeyEvent), +) -> anyhow::Result<()> { ratatui::run(|terminal| -> anyhow::Result<()> { let held = HeldLogs::new(); + let mut modes = Modes::default(); let mut redraw = true; + // Ticks keep time while events come: a stream of them must not + // keep a view from its background work. + let mut next_tick = Instant::now() + TICK; while !screen.done() { if redraw { terminal.draw(|f| screen.render(f))?; + // Asked once the screen is up: its modes are its own. + modes.ask(screen); } - if !event::poll(TICK)? { - redraw = screen.tick(); - continue; + if let Some(notice) = screen.take_notice() { + notify(¬ice); } - redraw = true; - if let Event::Key(key) = event::read()? { - if key.kind != KeyEventKind::Press { - continue; - } - if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') { - screen.interrupt(); - } else { - screen.handle_key(key); + redraw = false; + if event::poll(next_tick.saturating_duration_since(Instant::now()))? { + loop { + redraw |= match event::read()? { + Event::Key(key) if key.kind != KeyEventKind::Press => false, + Event::Key(key) + if key.modifiers.contains(KeyModifiers::CONTROL) + && key.code == KeyCode::Char('c') => + { + screen.interrupt(); + true + } + Event::Key(key) => { + on_key(screen, key); + true + } + Event::Mouse(m) => screen.mouse(m), + Event::Resize(..) => true, + _ => false, + }; + if screen.done() + || screen.pending_work().is_some() + || !event::poll(Duration::ZERO)? + { + break; + } } } + if Instant::now() >= next_tick { + redraw |= screen.tick(); + next_tick = Instant::now() + TICK; + } if let Some(message) = screen.pending_work() { + modes.release(); ratatui::restore(); crate::aux::logging::hold_logs(false); eprintln!("{message}"); screen.do_work(); crate::aux::logging::hold_logs(true); *terminal = ratatui::try_init()?; + redraw = true; } } + modes.release(); // Restore before writing what was held, not after. ratatui::restore(); drop(held); diff --git a/src/sim/handlers.rs b/src/sim/handlers.rs index 0845aa6..8a3992f 100644 --- a/src/sim/handlers.rs +++ b/src/sim/handlers.rs @@ -492,7 +492,7 @@ pub fn run_simulate(cmd_args: &RunSimulateArgs) -> anyhow::Result<()> { .collect(); write_lines(&batch_out, &batch_memb_file)?; - info!("batch membership: {:?}", &batch_memb_file); + info!("batch membership: {:?}", batch_memb_file); let rows: Vec> = (0..sim_args.rows) .map(|i| i.to_string().into_boxed_str()) @@ -515,12 +515,12 @@ pub fn run_simulate(cmd_args: &RunSimulateArgs) -> anyhow::Result<()> { if let Some(ref node_probs) = sim.hierarchy_node_probs { let hierarchy_file = mtx_file.replace(".mtx.gz", ".hierarchy.parquet"); node_probs.to_parquet_with_names(&hierarchy_file, (Some(&rows), Some("feature")), None)?; - info!("wrote hierarchy node probabilities: {:?}", &hierarchy_file); + info!("wrote hierarchy node probabilities: {:?}", hierarchy_file); } info!( "wrote parameter files:\n{:?},\n{:?},\n{:?}", - &ln_batch_file, &dict_file, &prop_file + ln_batch_file, dict_file, prop_file ); if cmd_args.save_mtx { diff --git a/src/sparse_io_vector/batch.rs b/src/sparse_io_vector/batch.rs index 17a3dcf..e47fc9a 100644 --- a/src/sparse_io_vector/batch.rs +++ b/src/sparse_io_vector/batch.rs @@ -117,7 +117,7 @@ impl SparseIoVec { // silently permuted batch ids vs the label order — a latent bug for δ / // `AdjMethod::Batch` consumers. let mut batches_vec: Vec<_> = batches.into_iter().collect(); - batches_vec.sort_by(|a, b| a.0.to_string().cmp(&b.0.to_string())); + batches_vec.sort_by_key(|a| a.0.to_string()); // Schedule largest batches first (LPT) so the heavy tail overlaps with // the small batches. The canonical id rides along; `sort_by_key(idx)` // below restores canonical order. @@ -273,7 +273,7 @@ impl SparseIoVec { let mut col_to_batch = vec![0; ntot]; let mut sorted_batches: Vec<_> = batches.into_iter().collect(); - sorted_batches.sort_by(|a, b| a.0.to_string().cmp(&b.0.to_string())); + sorted_batches.sort_by_key(|a| a.0.to_string()); let mut batch_names = Vec::with_capacity(sorted_batches.len()); let mut batch_to_cols = Vec::with_capacity(sorted_batches.len()); diff --git a/src/sparse_io_vector/matched.rs b/src/sparse_io_vector/matched.rs index af3b6e2..8e35540 100644 --- a/src/sparse_io_vector/matched.rs +++ b/src/sparse_io_vector/matched.rs @@ -63,7 +63,7 @@ impl SparseIoVec { { let (matched, matched_distances) = source_lookup.match_by_query_name_against(&glob, knn, target_lookup)?; - for (glob_matched, dist) in matched.into_iter().zip(matched_distances.into_iter()) { + for (glob_matched, dist) in matched.into_iter().zip(matched_distances) { if glob == glob_matched { continue; // avoid identical cell pairs } @@ -235,9 +235,7 @@ impl SparseIoVec { knn_columns, target_lookup, )?; - for (glob_matched_index, dist) in - matched.into_iter().zip(matched_distances.into_iter()) - { + for (glob_matched_index, dist) in matched.into_iter().zip(matched_distances) { if glob_index == glob_matched_index { continue; } diff --git a/tests/sparse_zarr_backend_tests.rs b/tests/sparse_zarr_backend_tests.rs index 9ffb01b..323b23a 100644 --- a/tests/sparse_zarr_backend_tests.rs +++ b/tests/sparse_zarr_backend_tests.rs @@ -39,7 +39,7 @@ fn dense_to_triplets_by_rows(mat: &Array2, rows: &[usize]) -> Vec<(u64, u64 } fn sort_triplets(mut t: Vec<(u64, u64, f32)>) -> Vec<(u64, u64, f32)> { - t.sort_by(|a, b| (a.1, a.0).cmp(&(b.1, b.0))); + t.sort_by_key(|a| (a.1, a.0)); t } diff --git a/tests/streaming_write_tests.rs b/tests/streaming_write_tests.rs index 7a18dde..7c5e9a9 100644 --- a/tests/streaming_write_tests.rs +++ b/tests/streaming_write_tests.rs @@ -68,7 +68,7 @@ fn stream_write( } fn sorted(mut t: Vec<(u64, u64, f32)>) -> Vec<(u64, u64, f32)> { - t.sort_by(|a, b| (a.1, a.0).cmp(&(b.1, b.0))); + t.sort_by_key(|a| (a.1, a.0)); t }