diff --git a/src/lib.rs b/src/lib.rs index fe7714b..cf32888 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,6 @@ +#![feature(char_min)] #![feature(exit_status_error)] +#![feature(step_trait)] #![feature(try_blocks)] #![feature(iter_intersperse)] diff --git a/src/main.rs b/src/main.rs index 92f1e8c..2726ccf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,7 +38,7 @@ use ratatui::{ }; use redu::{ cache::{self, filetree::SizeTree, Cache, Migrator}, - restic::{self, Restic, Snapshot}, + restic::{self, escape_for_exclude, Restic, Snapshot}, }; use scopeguard::defer; use thiserror::Error; @@ -216,7 +216,7 @@ fn main() -> anyhow::Result<()> { ) }; - let mut output_lines = vec![]; + let mut output_paths = vec![]; render(&mut terminal, &app)?; 'outer: loop { @@ -229,8 +229,8 @@ fn main() -> anyhow::Result<()> { None } Action::Quit => break 'outer, - Action::Generate(lines) => { - output_lines = lines; + Action::Generate(paths) => { + output_paths = paths; break 'outer; } Action::GetParentEntries(path_id) => { @@ -264,8 +264,8 @@ fn main() -> anyhow::Result<()> { disable_raw_mode()?; stderr().execute(LeaveAlternateScreen)?; - for line in output_lines { - println!("{line}"); + for line in output_paths { + println!("{}", escape_for_exclude(line.as_str())); } Ok(()) } diff --git a/src/restic.rs b/src/restic.rs index e76229a..4c800c8 100644 --- a/src/restic.rs +++ b/src/restic.rs @@ -1,7 +1,9 @@ use std::{ + borrow::Cow, ffi::OsStr, fmt::{Display, Formatter}, io::{BufRead, BufReader, Lines, Read}, + iter::Step, marker::PhantomData, os::unix::process::CommandExt, process::{Child, ChildStdout, Command, ExitStatusError, Stdio}, @@ -299,3 +301,71 @@ pub struct File { pub path: Utf8PathBuf, pub size: usize, } + +pub fn escape_for_exclude(path: &str) -> Cow { + fn is_special(c: char) -> bool { + ['*', '?', '[', '\\', '\r', '\n'].contains(&c) + } + + fn push_as_inverse_range(buf: &mut String, c: char) { + #[rustfmt::skip] + let cs = [ + '[', '^', + char::MIN, '-', char::backward(c, 1), + char::forward(c, 1), '-', char::MAX, + ']', + ]; + for d in cs { + buf.push(d); + } + } + + match path.find(is_special) { + None => Cow::Borrowed(path), + Some(index) => { + let (left, right) = path.split_at(index); + let mut escaped = String::with_capacity(path.len() + 1); // the +1 is for the extra \ + escaped.push_str(left); + for c in right.chars() { + match c { + '*' => escaped.push_str("[*]"), + '?' => escaped.push_str("[?]"), + '[' => escaped.push_str("[[]"), + '\\' => { + #[cfg(target_os = "windows")] + escaped.push('\\'); + #[cfg(not(target_os = "windows"))] + escaped.push_str("\\\\"); + } + '\r' => push_as_inverse_range(&mut escaped, '\r'), + '\n' => push_as_inverse_range(&mut escaped, '\n'), + c => escaped.push(c), + } + } + Cow::Owned(escaped) + } + } +} + +#[cfg(test)] +mod test { + use super::escape_for_exclude; + + #[cfg(not(target_os = "windows"))] + #[test] + fn escape_for_exclude_test() { + assert_eq!( + escape_for_exclude("foo* bar?[somethin\\g]]]\r\n"), + "foo[*] bar[?][[]somethin\\\\g]]][^\0-\u{000C}\u{000E}-\u{10FFFF}][^\0-\u{0009}\u{000B}-\u{10FFFF}]" + ); + } + + #[cfg(target_os = "windows")] + #[test] + fn escape_for_exclude_test() { + assert_eq!( + escape_for_exclude("foo* bar?[somethin\\g]]]\r\n"), + "foo[*] bar[?][[]somethin\\g]]][^\0-\u{000C}\u{000E}-\u{10FFFF}][^\0-\u{0009}\u{000B}-\u{10FFFF}]" + ); + } +} diff --git a/src/ui.rs b/src/ui.rs index 5c588d3..2cd393a 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -477,20 +477,42 @@ fn render_name( selected: bool, available_width: usize, ) -> Span { + let mut escaped = escape_name(name); if is_dir { - let mut name = Cow::Borrowed(name); - if !name.ends_with('/') { - name.to_mut().push('/'); + if !escaped.ends_with('/') { + escaped.to_mut().push('/'); } let span = - Span::raw(shorten_to(&name, available_width).into_owned()).bold(); + Span::raw(shorten_to(&escaped, available_width).into_owned()) + .bold(); if selected { span.dark_gray() } else { span.blue() } } else { - Span::raw(shorten_to(name, available_width)) + Span::raw(shorten_to(&escaped, available_width).into_owned()) + } +} + +fn escape_name(name: &str) -> Cow { + match name.find(char::is_control) { + None => Cow::Borrowed(name), + Some(index) => { + let (left, right) = name.split_at(index); + let mut escaped = String::with_capacity(name.len() + 1); // the +1 is for the extra \ + escaped.push_str(left); + for c in right.chars() { + if c.is_control() { + for d in c.escape_default() { + escaped.push(d); + } + } else { + escaped.push(c) + } + } + Cow::Owned(escaped) + } } } @@ -662,8 +684,6 @@ fn grapheme_len(s: &str) -> usize { /// Tests ////////////////////////////////////////////////////////////////////// #[cfg(test)] mod tests { - use std::borrow::Cow; - use super::{shorten_to, *}; #[test] @@ -687,6 +707,14 @@ mod tests { aux(0.5 + (7.0 / (8.0 * 16.0)), "████████▉ "); } + #[test] + fn escape_name_test() { + assert_eq!( + escape_name("f\no\\tóà 学校\r"), + Cow::Borrowed("f\\no\\tóà 学校\\r") + ); + } + #[test] fn shorten_to_test() { let s = "123456789";