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}