Skip to content

Commit

Permalink
Merge pull request #40 from drdo/escape-control-chars
Browse files Browse the repository at this point in the history
Escape control chars in the UI and when generating marks for restic exclude files
  • Loading branch information
drdo authored Jul 7, 2024
2 parents 3b61496 + 6d1f5e2 commit ff98e1e
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 13 deletions.
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#![feature(char_min)]
#![feature(exit_status_error)]
#![feature(step_trait)]
#![feature(try_blocks)]
#![feature(iter_intersperse)]

Expand Down
12 changes: 6 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -216,7 +216,7 @@ fn main() -> anyhow::Result<()> {
)
};

let mut output_lines = vec![];
let mut output_paths = vec![];

render(&mut terminal, &app)?;
'outer: loop {
Expand All @@ -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) => {
Expand Down Expand Up @@ -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(())
}
Expand Down
70 changes: 70 additions & 0 deletions src/restic.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand Down Expand Up @@ -299,3 +301,71 @@ pub struct File {
pub path: Utf8PathBuf,
pub size: usize,
}

pub fn escape_for_exclude(path: &str) -> Cow<str> {
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}]"
);
}
}
42 changes: 35 additions & 7 deletions src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<str> {
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)
}
}
}

Expand Down Expand Up @@ -662,8 +684,6 @@ fn grapheme_len(s: &str) -> usize {
/// Tests //////////////////////////////////////////////////////////////////////
#[cfg(test)]
mod tests {
use std::borrow::Cow;

use super::{shorten_to, *};

#[test]
Expand All @@ -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";
Expand Down

0 comments on commit ff98e1e

Please sign in to comment.