diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9fd45e0..6865543 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -16,6 +16,8 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Rustfmt Check + run: cargo fmt --check - name: Build run: cargo build --verbose - name: Run tests diff --git a/.gitignore b/.gitignore index 98c5dbd..64e017d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ -/target -/scripts/target +.DS_Store .idea +/scripts/target +/target diff --git a/benches/cache.rs b/benches/cache.rs index 3d38c3e..533a6b1 100644 --- a/benches/cache.rs +++ b/benches/cache.rs @@ -1,20 +1,21 @@ use std::cell::Cell; use std::fs; use std::path::PathBuf; -use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use uuid::Uuid; -use redu::cache::{Cache, SnapshotGroup}; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; use redu::cache::tests::*; +use redu::cache::{Cache, SnapshotGroup}; +use uuid::Uuid; pub fn criterion_benchmark(c: &mut Criterion) { - c.bench_function("merge filetree", |b| { - let filetree0 = Cell::new(generate_filetree(black_box(6), black_box(12))); - let filetree1 = Cell::new(generate_filetree(black_box(5), black_box(14))); + let filetree0 = + Cell::new(generate_filetree(black_box(6), black_box(12))); + let filetree1 = + Cell::new(generate_filetree(black_box(5), black_box(14))); b.iter(move || filetree0.take().merge(black_box(filetree1.take()))); }); - + c.bench_function("create and save group", |b| { let file: PathBuf = Uuid::new_v4().to_string().into(); { @@ -22,8 +23,8 @@ pub fn criterion_benchmark(c: &mut Criterion) { b.iter(move || { let mut group = SnapshotGroup::new(); group.add_snapshot( - "foo".into() - , generate_filetree(black_box(6), black_box(12)) + "foo".into(), + generate_filetree(black_box(6), black_box(12)), ); cache.save_snapshot_group(group).unwrap() }); diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..13d626f --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,8 @@ +max_width = 80 +match_arm_blocks = false +error_on_line_overflow = true +imports_granularity = "Module" +overflow_delimited_expr = true +group_imports = "StdExternalCrate" +use_small_heuristics = "Max" +use_field_init_shorthand = true diff --git a/src/cache/filetree.rs b/src/cache/filetree.rs index 7660f4a..4fd97ee 100644 --- a/src/cache/filetree.rs +++ b/src/cache/filetree.rs @@ -16,18 +16,14 @@ pub struct EntryExistsError; impl FileTree { pub fn new() -> FileTree { - FileTree { - size: 0, - children: HashMap::new(), - } + FileTree { size: 0, children: HashMap::new() } } pub fn insert( &mut self, path: &Utf8Path, size: usize, - ) -> Result<(), EntryExistsError> - { + ) -> Result<(), EntryExistsError> { let (mut breadcrumbs, remaining) = { let (breadcrumbs, remaining) = self.find(path); (breadcrumbs, remaining.map(Ok).unwrap_or(Err(EntryExistsError))?) @@ -38,7 +34,8 @@ impl FileTree { } let mut current = unsafe { &mut **breadcrumbs.last().unwrap() }; for c in remaining.iter() { - current = current.children.entry(Box::from(c)).or_insert(FileTree::new()); + current = + current.children.entry(Box::from(c)).or_insert(FileTree::new()); current.size = size; } Ok(()) @@ -46,8 +43,10 @@ impl FileTree { pub fn merge(self, other: FileTree) -> Self { fn sorted_children(filetree: FileTree) -> Vec<(Box, FileTree)> { - let mut children = filetree.children.into_iter().collect::>(); - children.sort_unstable_by(|(name0, _), (name1, _)| name0.cmp(name1)); + let mut children = + filetree.children.into_iter().collect::>(); + children + .sort_unstable_by(|(name0, _), (name1, _)| name0.cmp(name1)); children } @@ -65,21 +64,27 @@ impl FileTree { children.insert(name1, tree1); } } - (None, Some((name, tree))) => { children.insert(name, tree); } - (Some((name, tree)), None) => { children.insert(name, tree); } - (None, None) => { break; } + (None, Some((name, tree))) => { + children.insert(name, tree); + } + (Some((name, tree)), None) => { + children.insert(name, tree); + } + (None, None) => { + break; + } } } FileTree { size, children } } - + pub fn iter(&self) -> Iter { Iter { stack: vec![Breadcrumb { path: None, size: self.size, children: self.children.iter(), - }] + }], } } @@ -90,8 +95,7 @@ impl FileTree { fn find( &mut self, path: &Utf8Path, - ) -> (Vec<*mut FileTree>, Option) - { + ) -> (Vec<*mut FileTree>, Option) { let mut breadcrumbs: Vec<*mut FileTree> = vec![self]; let mut prefix = Utf8PathBuf::new(); for c in path.iter() { @@ -106,8 +110,11 @@ impl FileTree { } let remaining_path = { let suffix = path.strip_prefix(prefix).unwrap(); - if suffix.as_str().is_empty() { None } - else { Some(suffix.to_path_buf()) } + if suffix.as_str().is_empty() { + None + } else { + Some(suffix.to_path_buf()) + } }; (breadcrumbs, remaining_path) } @@ -139,31 +146,33 @@ impl<'a> Iterator for Iter<'a> { loop { match self.stack.pop() { None => break None, - Some(mut breadcrumb) => { - match breadcrumb.children.next() { - None => - break breadcrumb.path.map(|p| { - let dir = Directory{ path: p, size: breadcrumb.size }; - Entry::Directory(dir) - }), - Some((name, tree)) => { - let new_path = breadcrumb.path_extend(name); - self.stack.push(breadcrumb); - if tree.children.is_empty() { - break Some(Entry::File(File { path: new_path, size: tree.size })) - } else { - let new_breadcrumb = { - Breadcrumb { - path: Some(new_path), - size: tree.size, - children: tree.children.iter(), - } - }; - self.stack.push(new_breadcrumb); - } + Some(mut breadcrumb) => match breadcrumb.children.next() { + None => + break breadcrumb.path.map(|p| { + let dir = + Directory { path: p, size: breadcrumb.size }; + Entry::Directory(dir) + }), + Some((name, tree)) => { + let new_path = breadcrumb.path_extend(name); + self.stack.push(breadcrumb); + if tree.children.is_empty() { + break Some(Entry::File(File { + path: new_path, + size: tree.size, + })); + } else { + let new_breadcrumb = { + Breadcrumb { + path: Some(new_path), + size: tree.size, + children: tree.children.iter(), + } + }; + self.stack.push(new_breadcrumb); } } - } + }, } } } @@ -207,7 +216,7 @@ mod tests { assert_eq!(filetree.insert("a/1/x/1".into(), 1), Ok(())); filetree } - + #[test] fn insert_uniques_0() { let mut entries = example_tree_0().iter().collect::>(); @@ -245,7 +254,7 @@ mod tests { Entry::File(File { path: "a/2/x/0".into(), size: 7 }), ]); } - + #[test] fn insert_existing() { let mut filetree = example_tree_0(); @@ -253,7 +262,7 @@ mod tests { assert_eq!(filetree.insert("a/0".into(), 1), Err(EntryExistsError)); assert_eq!(filetree.insert("a/0/z/0".into(), 1), Err(EntryExistsError)); } - + #[test] fn merge_test() { let filetree = example_tree_0().merge(example_tree_1()); @@ -275,7 +284,7 @@ mod tests { Entry::File(File { path: "a/2/x/0".into(), size: 7 }), ]); } - + #[test] fn merge_reflexivity() { assert_eq!(example_tree_0().merge(example_tree_0()), example_tree_0()); @@ -284,13 +293,17 @@ mod tests { #[test] fn merge_associativity() { - assert_eq!(example_tree_0().merge(example_tree_1()).merge(example_tree_2()), - example_tree_0().merge(example_tree_1().merge(example_tree_2()))); + assert_eq!( + example_tree_0().merge(example_tree_1()).merge(example_tree_2()), + example_tree_0().merge(example_tree_1().merge(example_tree_2())) + ); } - + #[test] fn merge_commutativity() { - assert_eq!(example_tree_0().merge(example_tree_1()), - example_tree_1().merge(example_tree_0())); + assert_eq!( + example_tree_0().merge(example_tree_1()), + example_tree_1().merge(example_tree_0()) + ); } } diff --git a/src/cache/mod.rs b/src/cache/mod.rs index d2829b3..af498de 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -3,8 +3,8 @@ use std::path::Path; use camino::{Utf8Path, Utf8PathBuf}; use log::trace; -use rusqlite::{Connection, params, Row}; use rusqlite::functions::FunctionFlags; +use rusqlite::{params, Connection, Row}; use crate::cache::filetree::FileTree; use crate::types::{Directory, Entry, File}; @@ -17,9 +17,11 @@ pub fn is_corruption_error(error: &rusqlite::Error) -> bool { rusqlite::ErrorCode::NotADatabase, ]; match error { - rusqlite::Error::SqliteFailure(rusqlite::ffi::Error { code, .. }, _) => - CORRUPTION_CODES.contains(code), - _ => false + rusqlite::Error::SqliteFailure( + rusqlite::ffi::Error { code, .. }, + _, + ) => CORRUPTION_CODES.contains(code), + _ => false, } } @@ -39,11 +41,9 @@ impl Cache { | FunctionFlags::SQLITE_INNOCUOUS, |ctx| { let path = Utf8Path::new(ctx.get_raw(0).as_str()?); - let parent = path - .parent() - .map(ToOwned::to_owned); + let parent = path.parent().map(ToOwned::to_owned); Ok(parent.map(|p| p.to_string())) - } + }, )?; conn.profile(Some(|stmt, duration| { trace!("SQL {stmt} (took {duration:#?})") @@ -52,10 +52,7 @@ impl Cache { Ok(Cache { conn }) } - pub fn get_snapshots( - &self, - ) -> Result>, rusqlite::Error> - { + pub fn get_snapshots(&self) -> Result>, rusqlite::Error> { self.conn .prepare("SELECT id FROM snapshots")? .query_map([], |row| row.get("id"))? @@ -68,15 +65,17 @@ impl Cache { pub fn get_max_file_sizes( &self, path: Option>, - ) -> Result, rusqlite::Error> - { + ) -> Result, rusqlite::Error> { let aux = |row: &Row| { let child_path = { - let child_path: Utf8PathBuf = row.get::<&str, String>("path")?.into(); + let child_path: Utf8PathBuf = + row.get::<&str, String>("path")?.into(); path.as_ref() .map(AsRef::as_ref) .clone() - .map(|p| child_path.strip_prefix(p.as_std_path()).unwrap().into()) + .map(|p| { + child_path.strip_prefix(p.as_std_path()).unwrap().into() + }) .unwrap_or(child_path) }; let size = row.get("size")?; @@ -129,24 +128,31 @@ impl Cache { pub fn save_snapshot_group( &mut self, group: SnapshotGroup, - ) -> Result<(), rusqlite::Error> - { + ) -> Result<(), rusqlite::Error> { let tx = self.conn.transaction()?; { - let group_id: u64 = tx.query_row_and_then( - r#"SELECT max("group")+1 FROM snapshots"#, - [], - |row| row.get::>(0))? + let group_id: u64 = tx + .query_row_and_then( + r#"SELECT max("group")+1 FROM snapshots"#, + [], + |row| row.get::>(0), + )? .unwrap_or(0); - let mut snapshot_stmt = tx.prepare(r#" + let mut snapshot_stmt = tx.prepare( + r#" INSERT INTO snapshots (id, "group") - VALUES (?, ?)"#)?; - let mut file_stmt = tx.prepare(" + VALUES (?, ?)"#, + )?; + let mut file_stmt = tx.prepare( + " INSERT INTO files (snapshot_group, path, size) - VALUES (?, ?, ?)")?; - let mut dir_stmt = tx.prepare("\ + VALUES (?, ?, ?)", + )?; + let mut dir_stmt = tx.prepare( + "\ INSERT INTO directories (snapshot_group, path, size) - VALUES (?, ?, ?)")?; + VALUES (?, ?, ?)", + )?; for id in group.snapshots.iter() { snapshot_stmt.execute(params![id, group_id])?; @@ -154,12 +160,10 @@ impl Cache { for entry in group.filetree.take().iter() { match entry { - Entry::File(File{ path, size}) => - file_stmt.execute( - params![group_id, path.into_string(), size])?, - Entry::Directory(Directory{ path, size}) => - dir_stmt.execute( - params![group_id, path.into_string(), size])?, + Entry::File(File { path, size }) => file_stmt + .execute(params![group_id, path.into_string(), size])?, + Entry::Directory(Directory { path, size }) => dir_stmt + .execute(params![group_id, path.into_string(), size])?, }; } } @@ -169,12 +173,11 @@ impl Cache { pub fn get_snapshot_group( &self, id: impl AsRef, - ) -> Result - { + ) -> Result { self.conn.query_row_and_then( r#"SELECT "group" FROM snapshots WHERE id = ?"#, [id.as_ref()], - |row| row.get(0) + |row| row.get(0), ) } @@ -182,10 +185,12 @@ impl Cache { let tx = self.conn.transaction()?; tx.execute(r#"DELETE FROM snapshots WHERE "group" = ?"#, [id])?; tx.execute(r#"DELETE FROM files WHERE snapshot_group = ?"#, [id])?; - tx.execute(r#"DELETE FROM directories WHERE snapshot_group = ?"#, [id])?; + tx.execute(r#"DELETE FROM directories WHERE snapshot_group = ?"#, [ + id, + ])?; tx.commit() } - + // Marks //////////////////////////////////////////////// pub fn get_marks(&self) -> Result, rusqlite::Error> { let mut stmt = self.conn.prepare("SELECT path FROM marks")?; @@ -195,19 +200,22 @@ impl Cache { result } - pub fn upsert_mark(&mut self, path: &Utf8Path) -> Result { + pub fn upsert_mark( + &mut self, + path: &Utf8Path, + ) -> Result { self.conn.execute( "INSERT INTO marks (path) VALUES (?) \ ON CONFLICT (path) DO NOTHING", - [path.as_str()] + [path.as_str()], ) } - pub fn delete_mark(&mut self, path: &Utf8Path) -> Result { - self.conn.execute( - "DELETE FROM marks WHERE path = ?", - [path.as_str()] - ) + pub fn delete_mark( + &mut self, + path: &Utf8Path, + ) -> Result { + self.conn.execute("DELETE FROM marks WHERE path = ?", [path.as_str()]) } pub fn delete_all_marks(&mut self) -> Result { @@ -232,7 +240,7 @@ impl SnapshotGroup { self.snapshots.push(id); self.filetree.replace(self.filetree.take().merge(filetree)); } - + pub fn count(&self) -> usize { self.snapshots.len() } @@ -266,18 +274,21 @@ pub mod tests { if child < self.branching_factor { let mut new_prefix = prefix.clone(); new_prefix.push(Utf8PathBuf::from(child.to_string())); - self.state.push((depth, prefix, child+1)); + self.state.push((depth, prefix, child + 1)); if depth == 1 { - break(Some(new_prefix)); + break (Some(new_prefix)); } else { - self.state.push((depth-1, new_prefix, 0)); + self.state.push((depth - 1, new_prefix, 0)); } } } } } - pub fn generate_filetree(depth: usize, branching_factor: usize) -> FileTree { + pub fn generate_filetree( + depth: usize, + branching_factor: usize, + ) -> FileTree { let mut filetree = FileTree::new(); for path in PathGenerator::new(depth, branching_factor) { filetree.insert(&path, 1).unwrap(); diff --git a/src/lib.rs b/src/lib.rs index 9009e14..d84884a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,6 @@ #![feature(try_blocks)] #![feature(iter_intersperse)] +pub mod cache; pub mod restic; pub mod types; -pub mod cache; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 8ecdeb1..748ce3c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,40 +1,40 @@ #![feature(panic_update_hook)] -use std::{fs, panic, thread}; use std::borrow::Cow; use std::collections::HashSet; use std::io::stderr; -use std::sync::{Arc, mpsc, Mutex}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::RecvTimeoutError; +use std::sync::{mpsc, Arc, Mutex}; use std::thread::ScopedJoinHandle; use std::time::{Duration, Instant}; +use std::{fs, panic, thread}; use camino::Utf8Path; use clap::{command, Parser}; use crossterm::event::{KeyCode, KeyModifiers}; +use crossterm::terminal::{ + disable_raw_mode, enable_raw_mode, EnterAlternateScreen, + LeaveAlternateScreen, +}; use crossterm::ExecutableCommand; -use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}; use directories::ProjectDirs; -use flexi_logger::{FileSpec, Logger, LogSpecification, WriteMode}; +use flexi_logger::{FileSpec, LogSpecification, Logger, WriteMode}; use indicatif::{ProgressBar, ProgressStyle}; use log::{error, info, trace}; -use ratatui::{CompletedFrame, Terminal}; use ratatui::backend::{Backend, CrosstermBackend}; use ratatui::layout::Size; use ratatui::style::Stylize; use ratatui::widgets::WidgetRef; -use scopeguard::defer; -use thiserror::Error; - -use redu::{cache, restic}; -use redu::cache::{Cache, SnapshotGroup}; +use ratatui::{CompletedFrame, Terminal}; use redu::cache::filetree::FileTree; +use redu::cache::{Cache, SnapshotGroup}; use redu::restic::Restic; +use redu::{cache, restic}; +use scopeguard::defer; +use thiserror::Error; -use crate::ui::Action; -use crate::ui::App; -use crate::ui::Event; +use crate::ui::{Action, App, Event}; mod ui; @@ -57,7 +57,7 @@ mod ui; /// /// NOTE: redu will never do any kind of modification to your repo. /// It's strictly read-only. -/// +/// /// Keybinds: /// Arrows or hjkl: Movement /// PgUp/PgDown or C-b/C-f: Page up / Page down @@ -81,7 +81,7 @@ struct Cli { How many restic subprocesses to spawn concurrently. If you get ssh-related errors or too much memory use - try lowering this.", + try lowering this." )] fetching_thread_count: usize, #[arg( @@ -110,25 +110,19 @@ struct Cli { fn main() -> anyhow::Result<()> { let cli = Cli::parse(); - let restic = Restic::new( - cli.repo, - cli.password_command - ); + let restic = Restic::new(cli.repo, cli.password_command); let dirs = ProjectDirs::from("eu", "drdo", "redu") .expect("unable to determine project directory"); - + let _logger = { let mut directory = dirs.data_local_dir().to_path_buf(); directory.push(Utf8Path::new("logs")); eprintln!("Logging to {:#?}", directory); - - let filespec = { - FileSpec::default() - .directory(directory) - .suppress_basename() - }; + + let filespec = + { FileSpec::default().directory(directory).suppress_basename() }; let spec = match cli.verbose { 0 => LogSpecification::info(), @@ -148,7 +142,8 @@ fn main() -> anyhow::Result<()> { }))?; } - let mut cache = { // Get config to determine repo id and open cache + let mut cache = { + // Get config to determine repo id and open cache let pb = new_pb("Getting restic config {spinner}"); let repo_id = restic.config()?.id; pb.finish(); @@ -159,9 +154,10 @@ fn main() -> anyhow::Result<()> { path }; - fs::create_dir_all(dirs.cache_dir()) - .expect(&format!("unable to create cache directory at {}", - dirs.cache_dir().to_string_lossy())); + fs::create_dir_all(dirs.cache_dir()).expect(&format!( + "unable to create cache directory at {}", + dirs.cache_dir().to_string_lossy() + )); eprintln!("Using cache file {cache_file:#?}"); match Cache::open(&cache_file) { @@ -177,8 +173,13 @@ fn main() -> anyhow::Result<()> { }.expect("unable to open cache file") }; - sync_snapshots(&restic, &mut cache, cli.fetching_thread_count, cli.group_size)?; - + sync_snapshots( + &restic, + &mut cache, + cli.fetching_thread_count, + cli.group_size, + )?; + // UI stderr().execute(EnterAlternateScreen)?; panic::update_hook(|prev, info| { @@ -201,11 +202,16 @@ fn main() -> anyhow::Result<()> { cache.get_max_file_sizes(None::<&str>)?, cache.get_marks().unwrap(), vec![ - "m".bold(), ":Mark ".into(), - "u".bold(), ":Unmark ".into(), - "c".bold(), ":ClearAllMarks ".into(), - "g".bold(), ":Generate ".into(), - "q".bold(), ":Quit".into(), + "m".bold(), + ":Mark ".into(), + "u".bold(), + ":Unmark ".into(), + "c".bold(), + ":ClearAllMarks ".into(), + "g".bold(), + ":Generate ".into(), + "q".bold(), + ":Quit".into(), ], ) }; @@ -217,24 +223,19 @@ fn main() -> anyhow::Result<()> { let mut o_event = convert_event(crossterm::event::read()?); while let Some(event) = o_event { o_event = match app.update(event) { - Action::Nothing => - None, + Action::Nothing => None, Action::Render => { render(&mut terminal, &app)?; None } - Action::Quit => - break 'outer, + Action::Quit => break 'outer, Action::Generate(lines) => { output_lines = lines; - break 'outer + break 'outer; } Action::GetEntries(path) => { let children = cache.get_max_file_sizes(path.as_deref())?; - Some(Event::Entries { - parent: path, - children - }) + Some(Event::Entries { parent: path, children }) } Action::UpsertMark(path) => { cache.upsert_mark(&path)?; @@ -271,16 +272,18 @@ fn sync_snapshots( let missing_snapshots: Vec> = { // Fetch snapshot list let pb = new_pb("Fetching repository snapshot list {spinner}"); - let repo_snapshots = restic.snapshots()? + let repo_snapshots = restic + .snapshots()? .into_iter() .map(|s| s.id) .collect::>>(); pb.finish(); - - // Delete snapshots from the DB that were deleted on the repo - let groups_to_delete = cache.get_snapshots()? + + // Delete snapshots from the DB that were deleted on the repo + let groups_to_delete = cache + .get_snapshots()? .into_iter() - .filter(|snapshot| ! repo_snapshots.contains(&snapshot)) + .filter(|snapshot| !repo_snapshots.contains(&snapshot)) .map(|snapshot_id| cache.get_snapshot_group(snapshot_id)) .collect::, rusqlite::Error>>()?; if groups_to_delete.len() > 0 { @@ -295,18 +298,24 @@ fn sync_snapshots( } let db_snapshots = cache.get_snapshots()?; - repo_snapshots.into_iter().filter(|s| ! db_snapshots.contains(s)).collect() + repo_snapshots + .into_iter() + .filter(|s| !db_snapshots.contains(s)) + .collect() }; let total_missing_snapshots = match missing_snapshots.len() { - 0 => { eprintln!("Snapshots up to date"); return Ok(()); }, + 0 => { + eprintln!("Snapshots up to date"); + return Ok(()); + } n => n, }; - + eprintln!("Fetching {} snapshots", total_missing_snapshots); let missing_queue = Queue::new(missing_snapshots); - + // Create progress indicators let pb = new_pb("{wide_bar} [{pos}/{len}] {msg} {spinner}"); pb.set_length(total_missing_snapshots as u64); @@ -318,7 +327,7 @@ fn sync_snapshots( pb.set_message(format!("({msg:>12})")) }) }; - + thread::scope(|scope| { let mut handles: Vec>> = Vec::new(); @@ -329,7 +338,7 @@ fn sync_snapshots( // Channel to funnel snapshots from the fetching threads to the grouping thread let (snapshot_sender, snapshot_receiver) = mpsc::sync_channel::<(Box, FileTree)>(2); - + // Start fetching threads for _ in 0..fetching_thread_count { let missing_queue = missing_queue.clone(); @@ -344,8 +353,8 @@ fn sync_snapshots( speed, should_quit.clone(), ) - .inspect_err(|_| should_quit.store(true, Ordering::SeqCst)) - .map_err(anyhow::Error::from) + .inspect_err(|_| should_quit.store(true, Ordering::SeqCst)) + .map_err(anyhow::Error::from) })); } // Drop the leftover channel so that the grouping thread @@ -355,7 +364,7 @@ fn sync_snapshots( // Channel to funnel groups from the grouping thread to the db thread let (group_sender, group_receiver) = mpsc::sync_channel::(1); - + // Start grouping thread handles.push({ let should_quit = should_quit.clone(); @@ -367,8 +376,8 @@ fn sync_snapshots( pb, should_quit.clone(), ) - .inspect_err(|_| should_quit.store(true, Ordering::SeqCst)) - .map_err(anyhow::Error::from) + .inspect_err(|_| should_quit.store(true, Ordering::SeqCst)) + .map_err(anyhow::Error::from) }) }); @@ -376,11 +385,7 @@ fn sync_snapshots( handles.push({ let should_quit = should_quit.clone(); scope.spawn(move || { - db_thread_body( - cache, - group_receiver, - should_quit.clone() - ) + db_thread_body(cache, group_receiver, should_quit.clone()) .inspect_err(|_| should_quit.store(true, Ordering::SeqCst)) .map_err(anyhow::Error::from) }) @@ -388,7 +393,7 @@ fn sync_snapshots( // Drop the senders that weren't moved into threads so that // the receivers can detect when everyone is done - + for handle in handles { handle.join().unwrap()? } @@ -410,8 +415,7 @@ fn fetching_thread_body( snapshot_sender: mpsc::SyncSender<(Box, FileTree)>, mut speed: Speed, should_quit: Arc, -) -> Result<(), FetchingThreadError> -{ +) -> Result<(), FetchingThreadError> { defer! { trace!("(fetching-thread) terminated") } trace!("(fetching-thread) started"); while let Some(snapshot) = missing_queue.pop() { @@ -421,20 +425,29 @@ fn fetching_thread_body( trace!("(fetching-thread) started fetching snapshot ({short_id})"); let start = Instant::now(); for r in files { - if should_quit.load(Ordering::SeqCst) { return Ok(()); } + if should_quit.load(Ordering::SeqCst) { + return Ok(()); + } let (file, bytes_read) = r?; speed.inc(bytes_read); - filetree.insert(&file.path, file.size) + filetree + .insert(&file.path, file.size) .expect("repeated entry in restic snapshot ls"); } - info!("(fetching-thread) snapshot fetched in {}s ({short_id})", - start.elapsed().as_secs_f64()); + info!( + "(fetching-thread) snapshot fetched in {}s ({short_id})", + start.elapsed().as_secs_f64() + ); trace!("(fetching-thread) got snapshot, sending ({short_id})"); - if should_quit.load(Ordering::SeqCst) { return Ok(()); } + if should_quit.load(Ordering::SeqCst) { + return Ok(()); + } let start = Instant::now(); snapshot_sender.send((snapshot.clone(), filetree)).unwrap(); - info!("(fetching-thread) waited {}s to send snapshot ({short_id})", - start.elapsed().as_secs_f64()); + info!( + "(fetching-thread) waited {}s to send snapshot ({short_id})", + start.elapsed().as_secs_f64() + ); trace!("(fetching-thread) snapshot sent ({short_id})"); } speed.stop(); @@ -453,53 +466,64 @@ fn grouping_thread_body( group_sender: mpsc::SyncSender, pb: ProgressBar, should_quit: Arc, -) -> Result<(), GroupingThreadError> -{ +) -> Result<(), GroupingThreadError> { defer! { trace!("(grouping-thread) terminated") } trace!("(grouping-thread) started"); let mut group = SnapshotGroup::new(); loop { trace!("(grouping-thread) waiting for snapshot"); - if should_quit.load(Ordering::SeqCst) { return Ok(()); } + if should_quit.load(Ordering::SeqCst) { + return Ok(()); + } let start = Instant::now(); // We wait with timeout to poll the should_quit periodically match snapshot_receiver.recv_timeout(Duration::from_millis(500)) { Ok((snapshot, filetree)) => { let short_id = snapshot_short_id(&snapshot); - info!("(grouping-thread) waited {}s to get snapshot ({short_id})", - start.elapsed().as_secs_f64()); + info!( + "(grouping-thread) waited {}s to get snapshot ({short_id})", + start.elapsed().as_secs_f64() + ); trace!("(grouping-thread) got snapshot ({short_id})"); - if should_quit.load(Ordering::SeqCst) { return Ok(()); } + if should_quit.load(Ordering::SeqCst) { + return Ok(()); + } group.add_snapshot(snapshot.clone(), filetree); pb.inc(1); trace!("(grouping-thread) added snapshot ({short_id})"); if group.count() == group_size { trace!("(grouping-thread) group is full, sending"); - if should_quit.load(Ordering::SeqCst) { return Ok(()); } + if should_quit.load(Ordering::SeqCst) { + return Ok(()); + } let start = Instant::now(); group_sender.send(group).unwrap(); - info!("(grouping-thread) waited {}s to send group", - start.elapsed().as_secs_f64()); + info!( + "(grouping-thread) waited {}s to send group", + start.elapsed().as_secs_f64() + ); trace!("(grouping-thread) sent group"); group = SnapshotGroup::new(); } } - Err(RecvTimeoutError::Timeout) => { - continue - } + Err(RecvTimeoutError::Timeout) => continue, Err(RecvTimeoutError::Disconnected) => { trace!("(grouping-thread) loop done"); - break + break; } } } if group.count() > 0 { trace!("(grouping-thread) sending leftover group"); - if should_quit.load(Ordering::SeqCst) { return Ok(()); } + if should_quit.load(Ordering::SeqCst) { + return Ok(()); + } let start = Instant::now(); group_sender.send(group).unwrap(); - info!("(grouping-thread) waited {}s to send leftover group", - start.elapsed().as_secs_f64()); + info!( + "(grouping-thread) waited {}s to send leftover group", + start.elapsed().as_secs_f64() + ); trace!("(grouping-thread) sent leftover group"); } pb.finish_with_message("Done"); @@ -516,39 +540,45 @@ fn db_thread_body( cache: &mut Cache, group_receiver: mpsc::Receiver, should_quit: Arc, -) -> Result<(), DBThreadError> -{ +) -> Result<(), DBThreadError> { defer! { trace!("(db-thread) terminated") } trace!("(db-thread) started"); loop { trace!("(db-thread) waiting for group"); - if should_quit.load(Ordering::SeqCst) { return Ok(()); } + if should_quit.load(Ordering::SeqCst) { + return Ok(()); + } let start = Instant::now(); // We wait with timeout to poll the should_quit periodically match group_receiver.recv_timeout(Duration::from_millis(500)) { Ok(group) => { - info!("(db-thread) waited {}s to get group", - start.elapsed().as_secs_f64()); + info!( + "(db-thread) waited {}s to get group", + start.elapsed().as_secs_f64() + ); trace!("(db-thread) got group, saving"); - if should_quit.load(Ordering::SeqCst) { return Ok(()); } + if should_quit.load(Ordering::SeqCst) { + return Ok(()); + } let start = Instant::now(); - cache.save_snapshot_group(group) + cache + .save_snapshot_group(group) .expect("unable to save snapshot group"); - info!("(db-thread) waited {}s to save group", - start.elapsed().as_secs_f64()); + info!( + "(db-thread) waited {}s to save group", + start.elapsed().as_secs_f64() + ); trace!("(db-thread) group saved"); } - Err(RecvTimeoutError::Timeout) => { - continue - } + Err(RecvTimeoutError::Timeout) => continue, Err(RecvTimeoutError::Disconnected) => { trace!("(db-thread) loop done"); - break Ok(()) + break Ok(()); } } } } - + fn convert_event(event: crossterm::event::Event) -> Option { use crossterm::event::Event as TermEvent; use crossterm::event::KeyEventKind::{Press, Release}; @@ -557,22 +587,16 @@ fn convert_event(event: crossterm::event::Event) -> Option { const KEYBINDINGS: &[((KeyModifiers, KeyCode), Event)] = &[ ((KeyModifiers::empty(), KeyCode::Left), Left), ((KeyModifiers::empty(), KeyCode::Char('h')), Left), - ((KeyModifiers::empty(), KeyCode::Right), Right), ((KeyModifiers::empty(), KeyCode::Char(';')), Right), - ((KeyModifiers::empty(), KeyCode::Up), Up), ((KeyModifiers::empty(), KeyCode::Char('k')), Up), - ((KeyModifiers::empty(), KeyCode::Down), Down), ((KeyModifiers::empty(), KeyCode::Char('j')), Down), - ((KeyModifiers::empty(), KeyCode::PageUp), PageUp), ((KeyModifiers::CONTROL, KeyCode::Char('b')), PageUp), - ((KeyModifiers::empty(), KeyCode::PageDown), PageDown), ((KeyModifiers::CONTROL, KeyCode::Char('f')), PageDown), - ((KeyModifiers::empty(), KeyCode::Char('m')), Mark), ((KeyModifiers::empty(), KeyCode::Char('u')), Unmark), ((KeyModifiers::empty(), KeyCode::Char('c')), UnmarkAll), @@ -580,19 +604,15 @@ fn convert_event(event: crossterm::event::Event) -> Option { ((KeyModifiers::empty(), KeyCode::Char('g')), Generate), ]; match event { - TermEvent::Resize(w, h) => - Some(Resize(Size::new(w, h))), - TermEvent::Key(event) if [Press, Release].contains(&event.kind) => { - KEYBINDINGS - .iter() - .find_map(|((mods, code), ui_event)| - if event.modifiers == *mods && event.code == *code { - Some(ui_event.clone()) - } else { - None - } - ) - } + TermEvent::Resize(w, h) => Some(Resize(Size::new(w, h))), + TermEvent::Key(event) if [Press, Release].contains(&event.kind) => + KEYBINDINGS.iter().find_map(|((mods, code), ui_event)| { + if event.modifiers == *mods && event.code == *code { + Some(ui_event.clone()) + } else { + None + } + }), _ => None, } } @@ -641,10 +661,14 @@ impl Speed { let value = { let SpeedState { should_quit, count, previous } = &mut *state.lock().unwrap(); - if *should_quit { break; } - let current = *count as f64 / (WINDOW_MILLIS as f64 / 1000.0); + if *should_quit { + break; + } + let current = + *count as f64 / (WINDOW_MILLIS as f64 / 1000.0); *count = 0; - let value = (ALPHA * current) + ((1.0-ALPHA) * *previous); + let value = + (ALPHA * current) + ((1.0 - ALPHA) * *previous); *previous = current; value }; @@ -659,14 +683,13 @@ impl Speed { pub fn inc(&self, delta: usize) { self.state.lock().unwrap().count += delta; } - + pub fn stop(&mut self) { self.state.lock().unwrap().should_quit = true; } } -pub fn new_pb(style: &str) -> ProgressBar -{ +pub fn new_pb(style: &str) -> ProgressBar { let pb = ProgressBar::new_spinner() .with_style(ProgressStyle::with_template(style).unwrap()); pb.enable_steady_tick(Duration::from_millis(500)); diff --git a/src/restic.rs b/src/restic.rs index 24d1e7e..8244b5a 100644 --- a/src/restic.rs +++ b/src/restic.rs @@ -80,10 +80,7 @@ impl Display for Error { impl From for Error { fn from(value: LaunchError) -> Self { - Error { - kind: ErrorKind::Launch(value.into()), - stderr: None, - } + Error { kind: ErrorKind::Launch(value.into()), stderr: None } } } @@ -98,11 +95,7 @@ pub struct Restic { } impl Restic { - pub fn new( - repo: Option, - password_command: Option, - ) -> Self - { + pub fn new(repo: Option, password_command: Option) -> Self { Restic { repo, password_command } } @@ -117,8 +110,10 @@ impl Restic { pub fn ls( &self, snapshot: &str, - ) -> Result> + 'static, LaunchError> - { + ) -> Result< + impl Iterator> + 'static, + LaunchError, + > { fn parse_file(mut v: Value) -> Option { let mut m = std::mem::take(v.as_object_mut()?); Some(File { @@ -127,23 +122,26 @@ impl Restic { }) } - Ok(self.run_lazy_command(["ls", snapshot])? - .filter_map(|r| r - .map(|(value, bytes_read)| - parse_file(value).map(|file| (file, bytes_read)) - ) - .transpose())) + Ok(self.run_lazy_command(["ls", snapshot])?.filter_map(|r| { + r.map(|(value, bytes_read)| { + parse_file(value).map(|file| (file, bytes_read)) + }) + .transpose() + })) } // This is a trait object because of // https://github.com/rust-lang/rust/issues/125075 fn run_lazy_command( &self, - args: impl IntoIterator, - ) -> Result> + 'static>, LaunchError> + args: impl IntoIterator, + ) -> Result< + Box> + 'static>, + LaunchError, + > where T: DeserializeOwned + 'static, - A: AsRef + A: AsRef, { let child = self.run_command(args)?; Ok(Box::new(Iter::new(child))) @@ -151,18 +149,17 @@ impl Restic { fn run_greedy_command( &self, - args: impl IntoIterator, + args: impl IntoIterator, ) -> Result where T: DeserializeOwned, A: AsRef, { let child = self.run_command(args)?; - let output = child.wait_with_output() - .map_err(|e| Error { - kind: ErrorKind::Run(RunError::Io(e)), - stderr: None - })?; + let output = child.wait_with_output().map_err(|e| Error { + kind: ErrorKind::Run(RunError::Io(e)), + stderr: None, + })?; let r_value = try { output.status.exit_ok()?; serde_json::from_str(std::str::from_utf8(&output.stdout)?)? @@ -170,7 +167,9 @@ impl Restic { match r_value { Err(kind) => Err(Error { kind, - stderr: Some(String::from_utf8_lossy(&output.stderr).into_owned()), + stderr: Some( + String::from_utf8_lossy(&output.stderr).into_owned(), + ), }), Ok(value) => Ok(value), } @@ -178,12 +177,16 @@ impl Restic { fn run_command>( &self, - args: impl IntoIterator, - ) -> Result - { + args: impl IntoIterator, + ) -> Result { let mut cmd = Command::new("restic"); // Need to detach process from terminal - unsafe { cmd.pre_exec(|| { nix::unistd::setsid()?; Ok(()) }); } + unsafe { + cmd.pre_exec(|| { + nix::unistd::setsid()?; + Ok(()) + }); + } if let Some(repo) = &self.repo { cmd.arg("--repo").arg(repo); } @@ -217,18 +220,14 @@ impl Iter { } } - fn read_stderr(&mut self, kind: ErrorKind) -> Result - { + fn read_stderr(&mut self, kind: ErrorKind) -> Result { let mut buf = String::new(); match self.child.stderr.take().unwrap().read_to_string(&mut buf) { Err(e) => Err(Error { kind: ErrorKind::Run(RunError::Io(e)), stderr: None, }), - Ok(_) => Err(Error { - kind, - stderr: Some(buf), - }) + Ok(_) => Err(Error { kind, stderr: Some(buf) }), } } } @@ -249,11 +248,12 @@ impl Iterator for Iter { }) } else { match self.child.wait() { - Err(e) => Some(self.read_stderr(ErrorKind::Run(RunError::Io(e)))), + Err(e) => + Some(self.read_stderr(ErrorKind::Run(RunError::Io(e)))), Ok(status) => match status.exit_ok() { Err(e) => Some(self.read_stderr(e.into())), Ok(()) => None, - } + }, } } } diff --git a/src/ui.rs b/src/ui.rs index 1e895a8..f369fe8 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; -use std::iter; use std::cmp::{max, min}; use std::collections::HashSet; +use std::iter; use camino::{Utf8Path, Utf8PathBuf}; use ratatui::buffer::Buffer; @@ -10,22 +10,25 @@ use ratatui::prelude::Line; use ratatui::style::{Style, Stylize}; use ratatui::text::Span; use ratatui::widgets::{List, ListItem, Paragraph, WidgetRef}; -use unicode_segmentation::UnicodeSegmentation; - use redu::types::{Directory, Entry, File}; +use unicode_segmentation::UnicodeSegmentation; #[derive(Clone, Debug)] pub enum Event { Resize(Size), - Left, Right, - Up, Down, - PageUp, PageDown, + Left, + Right, + Up, + Down, + PageUp, + PageDown, Mark, Unmark, UnmarkAll, Quit, Generate, - Entries { /// `children` is expected to be sorted by size, largest first. + Entries { + /// `children` is expected to be sorted by size, largest first. parent: Option, children: Vec, }, @@ -78,8 +81,7 @@ impl App { } } - pub fn update(&mut self, event: Event) -> Action - { + pub fn update(&mut self, event: Event) -> Action { log::debug!("received {:?}", event); use Event::*; match event { @@ -110,8 +112,7 @@ impl App { fn left(&mut self) -> Action { match &self.path { - None => - Action::Nothing, + None => Action::Nothing, Some(path) => Action::GetEntries(path.parent().map(Utf8Path::to_path_buf)), } @@ -120,7 +121,7 @@ impl App { fn right(&mut self) -> Action { if !self.entries.is_empty() { match &self.entries[self.selected] { - Entry::Directory(Directory{ path, .. }) => { + Entry::Directory(Directory { path, .. }) => { let new_path = path_extended(self.path.as_deref(), &path); Action::GetEntries(Some(new_path.into_owned())) } @@ -132,31 +133,28 @@ impl App { } fn move_selection(&mut self, delta: isize, wrap: bool) -> Action { - if self.entries.is_empty() { return Action::Nothing } + if self.entries.is_empty() { + return Action::Nothing; + } let selected = self.selected as isize; let len = self.entries.len() as isize; - self.selected = - if wrap { - (selected + delta).rem_euclid(len) - } else { - max(0, min(len-1, selected + delta)) - } as usize; + self.selected = if wrap { + (selected + delta).rem_euclid(len) + } else { + max(0, min(len - 1, selected + delta)) + } as usize; self.fix_offset(); Action::Render } fn mark_selection(&mut self) -> Action { - self.selected_entry() - .map(Action::UpsertMark) - .unwrap_or(Action::Nothing) + self.selected_entry().map(Action::UpsertMark).unwrap_or(Action::Nothing) } fn unmark_selection(&mut self) -> Action { - self.selected_entry() - .map(Action::DeleteMark) - .unwrap_or(Action::Nothing) + self.selected_entry().map(Action::DeleteMark).unwrap_or(Action::Nothing) } fn unmark_all(&self) -> Action { @@ -164,7 +162,8 @@ impl App { } fn generate(&self) -> Action { - let mut lines = self.marks + let mut lines = self + .marks .iter() .map(|p| Box::from(p.as_str())) .collect::>(); @@ -175,17 +174,15 @@ impl App { fn set_entries( &mut self, parent: Option, - entries: Vec - ) -> Action - { - self.selected = - entries - .iter() - .map(|e| path_extended(parent.as_deref(), e.path())) - .enumerate() - .find(|(_, path)| Some(path.as_ref()) == self.path.as_deref()) - .map(|(i, _)| i) - .unwrap_or(0); + entries: Vec, + ) -> Action { + self.selected = entries + .iter() + .map(|e| path_extended(parent.as_deref(), e.path())) + .enumerate() + .find(|(_, path)| Some(path.as_ref()) == self.path.as_deref()) + .map(|(i, _)| i) + .unwrap_or(0); self.offset = 0; self.path = parent; self.entries = entries; @@ -204,33 +201,34 @@ impl App { let h = self.list_size.height as isize; let first_visible = offset; let last_visible = offset + h - 1; - let new_offset = - if selected < first_visible { - selected - } else if last_visible < selected { - selected - h + 1 - } else { - offset - }; + let new_offset = if selected < first_visible { + selected + } else if last_visible < selected { + selected - h + 1 + } else { + offset + }; self.offset = new_offset as usize; } fn selected_entry(&self) -> Option { - if self.entries.is_empty() { return None } + if self.entries.is_empty() { + return None; + } let full_path = path_extended( self.path.as_deref(), - self.entries[self.selected].path() - ).into_owned(); + self.entries[self.selected].path(), + ) + .into_owned(); Some(full_path) } } fn path_extended<'a>( o_path: Option<&Utf8Path>, - more: &'a Utf8Path -) -> Cow<'a, Utf8Path> -{ + more: &'a Utf8Path, +) -> Cow<'a, Utf8Path> { match o_path { None => Cow::Borrowed(more), Some(path) => { @@ -256,71 +254,85 @@ impl ListEntry { let mut spans = Vec::with_capacity(4); // Mark - spans.push(Span::raw( - if self.is_marked { "*" } - else { " " } - )); + spans.push(Span::raw(if self.is_marked { "*" } else { " " })); // Size - spans.push(Span::raw( - format!(" {:>10}", humansize::format_size(self.size, humansize::BINARY)) - )); + spans.push(Span::raw(format!( + " {:>10}", + humansize::format_size(self.size, humansize::BINARY) + ))); // Bar - spans.push(Span::raw({ - const MAX_BAR_WIDTH: usize = 16; - let bar_frac_width = (self.relative_size * (MAX_BAR_WIDTH*8) as f64) as usize; - let full_blocks = bar_frac_width / 8; - let last_block = match (bar_frac_width % 8) as u32 { - 0 => String::new(), - x => String::from(unsafe { char::from_u32_unchecked(0x2590 - x) }), - }; - let empty_width = MAX_BAR_WIDTH - - full_blocks - - last_block.graphemes(true).count(); - let mut bar = String::with_capacity(1+MAX_BAR_WIDTH+1); - bar.push(' '); - for _ in 0..full_blocks { bar.push('\u{2588}'); } - bar.push_str(&last_block); - for _ in 0..empty_width { bar.push(' '); } - bar.push(' '); - bar - }).green()); + spans.push( + Span::raw({ + const MAX_BAR_WIDTH: usize = 16; + let bar_frac_width = + (self.relative_size * (MAX_BAR_WIDTH * 8) as f64) as usize; + let full_blocks = bar_frac_width / 8; + let last_block = match (bar_frac_width % 8) as u32 { + 0 => String::new(), + x => String::from(unsafe { + char::from_u32_unchecked(0x2590 - x) + }), + }; + let empty_width = MAX_BAR_WIDTH + - full_blocks + - last_block.graphemes(true).count(); + let mut bar = String::with_capacity(1 + MAX_BAR_WIDTH + 1); + bar.push(' '); + for _ in 0..full_blocks { + bar.push('\u{2588}'); + } + bar.push_str(&last_block); + for _ in 0..empty_width { + bar.push(' '); + } + bar.push(' '); + bar + }) + .green(), + ); // Name spans.push({ let available_width = { - let used: usize = spans.iter() + let used: usize = spans + .iter() .map(|s| s.content.graphemes(true).count()) .sum(); max(0, width as isize - used as isize) as usize }; - if self.is_dir - { + if self.is_dir { let mut name = Cow::Borrowed(self.name.as_str()); if name.chars().last() != Some('/') { name.to_mut().push('/'); } - let span = Span::raw(shorten_to(&name, available_width).into_owned()) - .bold(); - if selected { span.dark_gray() } - else { span.blue() } + let span = + Span::raw(shorten_to(&name, available_width).into_owned()) + .bold(); + if selected { + span.dark_gray() + } else { + span.blue() + } } else { Span::raw(shorten_to(self.name.as_str(), available_width)) } }); - Line::from(spans).style( - if selected { Style::new().black().on_white() } - else { Style::new() } - ) + Line::from(spans).style(if selected { + Style::new().black().on_white() + } else { + Style::new() + }) } } impl WidgetRef for App { fn render_ref(&self, area: Rect, buf: &mut Buffer) { let (header_rect, list_rect, footer_rect) = compute_layout(area); - { // Heading + { + // Heading let mut string = "--- ".to_string(); string.push_str( shorten_to( @@ -328,51 +340,54 @@ impl WidgetRef for App { None => "#", Some(path) => path.as_str(), }, - header_rect.width as usize - string.len() - ).as_ref() + header_rect.width as usize - string.len(), + ) + .as_ref(), ); let mut remaining_width = max( 0, - header_rect.width as isize - string.graphemes(true).count() as isize + header_rect.width as isize + - string.graphemes(true).count() as isize, ) as usize; if remaining_width > 0 { string.push(' '); remaining_width -= 1; } string.push_str(&"-".repeat(remaining_width)); - Paragraph::new(string) - .on_light_blue() - .render_ref(header_rect, buf); + Paragraph::new(string).on_light_blue().render_ref(header_rect, buf); } - { // List + { + // List let list_entries = to_list_entries( - |p| self.marks.contains( - path_extended(self.path.as_deref(), p).as_ref() - ), + |p| { + self.marks.contains( + path_extended(self.path.as_deref(), p).as_ref(), + ) + }, self.entries.iter(), ); - let items = list_entries - .iter() - .enumerate() - .skip(self.offset) - .map(|(index, entry)| { ListItem::new( - entry.to_line( - self.list_size.width, - index == self.selected - ) - )}); + let items = + list_entries.iter().enumerate().skip(self.offset).map( + |(index, entry)| { + ListItem::new(entry.to_line( + self.list_size.width, + index == self.selected, + )) + }, + ); List::new(items).render_ref(list_rect, buf) } - { // Footer + { + // Footer let spans = vec![ Span::from(format!(" Marks: {}", self.marks.len())), Span::from(" | "), ] - .into_iter() - .chain(self.footer_extra.clone().into_iter()) - .collect::>(); + .into_iter() + .chain(self.footer_extra.clone().into_iter()) + .collect::>(); Paragraph::new(Line::from(spans)) .on_light_blue() .render_ref(footer_rect, buf); @@ -383,20 +398,19 @@ impl WidgetRef for App { /// `entries` is expected to be sorted by size, largest first. fn to_list_entries<'a>( mut is_marked: impl FnMut(&Utf8Path) -> bool, - entries: impl IntoIterator, -) -> Vec -{ + entries: impl IntoIterator, +) -> Vec { let mut entries = entries.into_iter(); match entries.next() { None => Vec::new(), Some(first) => { let largest = first.size() as f64; - iter::once(first).chain(entries) + iter::once(first) + .chain(entries) .map(|e| { let (path, size, is_dir) = match e { - Entry::File(File{ path, size }) => - (path, size, false), - Entry::Directory(Directory{ path, size }) => + Entry::File(File { path, size }) => (path, size, false), + Entry::Directory(Directory { path, size }) => (path, size, true), }; ListEntry { @@ -416,8 +430,7 @@ fn shorten_to(s: &str, width: usize) -> Cow { let len = s.graphemes(true).count(); let res = if len <= width { Cow::Borrowed(s) - } - else if width <= 3 { + } else if width <= 3 { Cow::Owned(".".repeat(width)) } else { let front_width = (width - 3).div_euclid(2); @@ -425,7 +438,9 @@ fn shorten_to(s: &str, width: usize) -> Cow { let graphemes = s.graphemes(true); let mut name = graphemes.clone().take(front_width).collect::(); name.push_str("..."); - for g in graphemes.skip(len-back_width) { name.push_str(g); } + for g in graphemes.skip(len - back_width) { + name.push_str(g); + } Cow::Owned(name) }; res @@ -455,9 +470,8 @@ fn compute_layout(area: Rect) -> (Rect, Rect, Rect) { #[cfg(test)] mod tests { use std::borrow::Cow; - use super::shorten_to; - use super::*; + use super::{shorten_to, *}; #[test] fn list_entry_to_line_narrow_width() { @@ -498,7 +512,7 @@ mod tests { ]) ); } - + #[test] fn list_entry_to_line_small_size_file() { let f = ListEntry { @@ -518,7 +532,7 @@ mod tests { ]) ); } - + #[test] fn list_entry_to_line_directory() { let f = ListEntry { @@ -535,11 +549,12 @@ mod tests { Span::raw(" 9.99 KiB"), Span::raw(" ██████████████▍ ").green(), Span::raw("1234567890123456789012345678901234567890/") - .bold().blue() + .bold() + .blue() ]) ); } - + #[test] fn list_entry_to_line_file_selected() { let f = ListEntry { @@ -556,10 +571,12 @@ mod tests { Span::raw(" 999.99 KiB"), Span::raw(" ██████████████▍ ").green(), Span::raw("1234567890123456789012345678901234567890") - ]).black().on_white() + ]) + .black() + .on_white() ); } - + #[test] fn list_entry_to_line_directory_selected() { let f = ListEntry { @@ -576,11 +593,14 @@ mod tests { Span::raw(" 9.99 KiB"), Span::raw(" ██████████████▍ ").green(), Span::raw("1234567890123456789012345678901234567890/") - .bold().dark_gray() - ]).black().on_white() + .bold() + .dark_gray() + ]) + .black() + .on_white() ); } - + #[test] fn list_entry_to_line_file_marked() { let f = ListEntry { @@ -600,7 +620,7 @@ mod tests { ]) ); } - + #[test] fn list_entry_to_line_file_marked_selected() { let f = ListEntry { @@ -617,7 +637,9 @@ mod tests { Span::raw(" 999.99 KiB"), Span::raw(" ██████████████▍ ").green(), Span::raw("1234567890123456789012345678901234567890") - ]).black().on_white() + ]) + .black() + .on_white() ); }