Watches a file and prints human-readable, coloured diffs.
at main 3.5 kB view raw
1use std::{fs::read_to_string, io::Write as _, path::PathBuf, sync::mpsc}; 2 3use clap::{Parser, ValueEnum}; 4use notify::{RecursiveMode, Watcher}; 5use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; 6 7/// Watch a file and print diffs of changes. 8#[derive(Debug, Parser)] 9#[command(version, about, long_about = None)] 10pub struct Args { 11 /// Path to the file to watch. 12 file: PathBuf, 13 14 /// Whether to enable color output. 15 #[arg(long, value_enum, default_value_t = ColorMode::Auto)] 16 color: ColorMode, 17 18 /// Show unchanged lines. 19 #[arg(long, default_value_t = false)] 20 show_unchanged: bool, 21} 22 23#[derive(Debug, Copy, Clone, PartialEq, Eq, ValueEnum)] 24enum ColorMode { 25 Auto, 26 Always, 27 Never, 28} 29 30impl From<ColorMode> for ColorChoice { 31 fn from(value: ColorMode) -> Self { 32 match value { 33 ColorMode::Auto => Self::Auto, 34 ColorMode::Always => Self::Always, 35 ColorMode::Never => Self::Never, 36 } 37 } 38} 39 40fn main() -> anyhow::Result<()> { 41 let args = Args::parse(); 42 let (tx, rx) = mpsc::channel(); 43 let mut stdout = StandardStream::stdout(args.color.into()); 44 45 let mut watcher = notify::recommended_watcher(tx)?; 46 watcher.watch(&args.file, RecursiveMode::NonRecursive)?; 47 48 let mut old_content = read_to_string(&args.file)?; 49 50 for evt in rx { 51 let evt = evt?; 52 53 match evt.kind { 54 notify::EventKind::Create(_) | notify::EventKind::Modify(_) => { 55 let new_content = read_to_string(&args.file)?; 56 57 if old_content == new_content { 58 // ignore 59 continue; 60 } 61 62 on_update(&mut stdout, &old_content, &new_content, args.show_unchanged)?; 63 old_content = new_content; 64 } 65 notify::EventKind::Remove(_) => { 66 stdout.reset()?; 67 writeln!(&mut stdout, "................ file deleted!")?; 68 break; 69 } 70 _ => {} 71 } 72 } 73 74 stdout.reset()?; 75 76 Ok(()) 77} 78 79fn on_update( 80 out: &mut impl WriteColor, 81 old_content: &str, 82 new_content: &str, 83 show_unchanged: bool, 84) -> std::io::Result<()> { 85 let color_deleted = { 86 let mut color = ColorSpec::new(); 87 color.set_fg(Some(Color::Red)); 88 color 89 }; 90 let color_unchanged = ColorSpec::new(); 91 let color_added = { 92 let mut color = ColorSpec::new(); 93 color.set_fg(Some(Color::Green)); 94 color 95 }; 96 let (mut left_line, mut right_line) = (0, 0); 97 98 out.reset()?; 99 writeln!(out, "................ file modified!")?; 100 for line in diff::lines(old_content, new_content) { 101 match line { 102 diff::Result::Left(l) => { 103 left_line += 1; 104 105 out.set_color(&color_deleted)?; 106 writeln!(out, "{left_line:>5} - {l}")?; 107 } 108 diff::Result::Both(l, _) => { 109 left_line += 1; 110 right_line += 1; 111 if show_unchanged { 112 out.set_color(&color_unchanged)?; 113 writeln!(out, "{left_line:>5} {right_line:>5} | {l}")?; 114 } 115 } 116 diff::Result::Right(l) => { 117 right_line += 1; 118 out.set_color(&color_added)?; 119 writeln!(out, " {right_line:>5} + {l}")?; 120 } 121 } 122 } 123 out.reset()?; 124 125 Ok(()) 126}