diff_folders/
app.rs

1use crate::status::{FolderStatefulList, StatefulList};
2use crossterm::event::KeyCode;
3use file_diff::diff;
4use similar::{ChangeTag, TextDiff};
5use std::collections::HashMap;
6use std::convert::From;
7use std::fs::File;
8use std::io::{self, BufRead, Read};
9use tui::layout::{Constraint, Direction, Layout};
10use tui::style::{Color, Modifier, Style};
11use tui::text::{Span, Spans};
12use tui::widgets::{Block, Borders, Gauge, List, ListItem, Paragraph};
13use tui::Terminal;
14use tui::{backend::Backend, Frame};
15use walkdir::DirEntry;
16
17enum WindowType {
18    Left,
19    Right,
20}
21pub struct App {
22    new_dir: String,
23    old_dir: String,
24    tab: WindowType,
25    items: StatefulList<FolderStatefulList>,
26
27    // window status
28    scroll: u16,
29    len_contents: usize,
30    cur_file_path: Option<FolderStatefulList>,
31
32    page_size: u16,
33    is_home: bool,
34    is_loaded: bool,
35}
36
37impl App {
38    pub fn new(old_dir: String, new_dir: String) -> Self {
39        Self {
40            new_dir,
41            old_dir,
42            tab: WindowType::Left,
43            scroll: 0,
44            len_contents: 0,
45            cur_file_path: None,
46            is_home: false,
47            is_loaded: false,
48            page_size: 0,
49            items: StatefulList::with_items(Vec::new()),
50        }
51    }
52
53    pub fn event(&mut self, key_code: KeyCode) {
54        match key_code {
55            KeyCode::Left => {
56                self.left();
57            }
58            KeyCode::Right => {
59                self.right();
60            }
61            KeyCode::Down => {
62                self.down();
63            }
64            KeyCode::Up => {
65                self.up();
66            }
67            KeyCode::PageUp => self.page_up(),
68            KeyCode::PageDown => self.page_down(),
69            KeyCode::Enter => self.enter(),
70            KeyCode::Home => self.home(),
71            _ => {}
72        }
73    }
74
75    fn left(&mut self) {
76        match self.tab {
77            WindowType::Right => self.tab = WindowType::Left,
78            _ => {}
79        }
80    }
81
82    fn right(&mut self) {
83        match self.tab {
84            WindowType::Left => self.tab = WindowType::Right,
85            _ => {}
86        }
87    }
88
89    fn up(&mut self) {
90        match self.tab {
91            WindowType::Left => {
92                self.items.previous(1);
93                self.enter();
94            }
95            WindowType::Right => {
96                if self.scroll > 0 {
97                    self.scroll -= 1
98                }
99            }
100        }
101    }
102
103    fn down(&mut self) {
104        match self.tab {
105            WindowType::Left => {
106                self.items.next(1);
107                self.enter();
108            }
109            WindowType::Right => {
110                let total = self.len_contents as u16;
111                if self.scroll >= total {
112                    self.scroll = total
113                } else {
114                    self.scroll += 1
115                }
116            }
117        }
118    }
119
120    fn enter(&mut self) {
121        self.is_home = false;
122        if let Some(file) = &self.cur_file_path {
123            if file.entry.path() == self.items.cur().entry.path() {
124                // same file
125                return;
126            }
127        }
128        self.cur_file_path = Some(self.items.cur().clone());
129        self.scroll = 0
130    }
131
132    fn home(&mut self) {
133        self.cur_file_path = Some(self.items.cur().clone());
134        self.is_home = true;
135    }
136
137    fn page_up(&mut self) {
138        match self.tab {
139            WindowType::Left => {
140                self.items.previous(self.page_size as usize);
141                self.enter();
142            }
143            WindowType::Right => {
144                let mut page_size = self.page_size;
145                let content_length = self.len_contents as u16;
146                if page_size > content_length {
147                    page_size = content_length;
148                }
149                if self.scroll < page_size {
150                    self.scroll = 0
151                } else {
152                    self.scroll -= page_size
153                }
154            }
155        }
156    }
157
158    fn page_down(&mut self) {
159        match self.tab {
160            WindowType::Left => {
161                self.items.next(self.page_size as usize);
162                self.enter();
163            }
164            WindowType::Right => {
165                let mut page_size = self.page_size;
166                let content_length = self.len_contents as u16;
167                if page_size > content_length {
168                    page_size = content_length;
169                }
170                if self.scroll + page_size >= content_length {
171                    self.scroll = content_length
172                } else {
173                    self.scroll += page_size
174                }
175            }
176        }
177    }
178
179    fn draw_gauge<B: Backend>(&mut self, terminal: &mut Terminal<B>) {
180        self.diff_list_dir(&mut move |p| {
181            let _ = terminal.draw(|f| {
182                let chunks = Layout::default()
183                    .direction(Direction::Vertical)
184                    .margin(1)
185                    .constraints(
186                        [
187                            Constraint::Percentage(40),
188                            Constraint::Length(5),
189                            Constraint::Percentage(40),
190                        ]
191                        .as_ref(),
192                    )
193                    .split(f.size());
194                let gauge = Gauge::default()
195                    .block(
196                        Block::default()
197                            .title("Loading files")
198                            .borders(Borders::ALL),
199                    )
200                    .gauge_style(Style::default().fg(Color::White))
201                    .percent(p);
202                f.render_widget(gauge, chunks[1]);
203            }); // loading files
204        });
205    }
206
207    pub fn draw_terminal<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> io::Result<()> {
208        if !self.is_loaded {
209            self.draw_gauge(terminal);
210            self.is_loaded = true;
211        }
212        terminal.draw(|f| self.draw(f))?;
213        return Ok(());
214    }
215
216    pub fn draw<B: Backend>(&mut self, f: &mut Frame<B>) {
217        let chunks = Layout::default()
218            .direction(Direction::Horizontal)
219            .margin(1)
220            .constraints(
221                match self.tab {
222                    WindowType::Left => [Constraint::Percentage(70), Constraint::Percentage(30)],
223                    WindowType::Right => [Constraint::Percentage(30), Constraint::Percentage(70)],
224                }
225                .as_ref(),
226            )
227            .split(f.size());
228
229        self.page_size = chunks[0].height / 2;
230
231        let items: Vec<ListItem> = self
232            .items
233            .items
234            .iter()
235            .map(|i| {
236                let path = match i.entry.path().to_str() {
237                    Some(p) => {
238                        let cur_path = p.replace(&self.new_dir, ".");
239                        if i.entry.path().is_dir() {
240                            format!("d {}", cur_path)
241                        } else {
242                            format!("f {}", cur_path)
243                        }
244                    }
245                    None => "".to_owned(),
246                };
247                let lines = vec![Spans::from(path)];
248                ListItem::new(lines).style(match i.state {
249                    crate::status::StatusItemType::Deleted => Style::default().fg(Color::Red),
250                    crate::status::StatusItemType::Modified => {
251                        Style::default().fg(Color::LightYellow)
252                    }
253                    crate::status::StatusItemType::New => Style::default().fg(Color::Green),
254                    crate::status::StatusItemType::Normal => Style::default(),
255                })
256            })
257            .collect();
258        let items = List::new(items)
259            .block(
260                Block::default()
261                    .borders(Borders::ALL)
262                    .border_style(match self.tab {
263                        WindowType::Left => Style::default().fg(Color::Gray),
264                        WindowType::Right => Style::default().fg(Color::Black),
265                    })
266                    .title(format!("folder {}", self.new_dir)),
267            )
268            .highlight_style(
269                Style::default()
270                    .bg(Color::LightBlue)
271                    .add_modifier(Modifier::BOLD)
272                    .add_modifier(Modifier::ITALIC),
273            );
274        f.render_stateful_widget(items, chunks[0], &mut self.items.state);
275
276        if let Some(file) = &self.cur_file_path {
277            let (contents, title) =
278                Self::get_diff_spans(file, &self.new_dir, &self.old_dir, self.is_home);
279            self.len_contents = contents.len() as usize;
280            let paragraph = Paragraph::new(contents)
281                .style(Style::default())
282                .block(
283                    Block::default()
284                        .borders(Borders::ALL)
285                        .border_style(match self.tab {
286                            WindowType::Left => Style::default().fg(Color::Black),
287                            WindowType::Right => Style::default().fg(Color::Gray),
288                        })
289                        .title(title),
290                )
291                .wrap(tui::widgets::Wrap { trim: false })
292                .scroll((self.scroll, 0));
293            f.render_widget(paragraph, chunks[1]);
294        }
295    }
296
297    fn get_diff_spans<'a>(
298        file: &FolderStatefulList,
299        new_dir: &'a str,
300        old_dir: &'a str,
301        is_home: bool,
302    ) -> (Vec<Spans<'a>>, String) {
303        if is_home {
304            return (
305                vec![Spans::from(String::from_utf8(MSG.to_vec()).unwrap())],
306                "letter".to_string(),
307            );
308        }
309        if file.entry.path().is_dir() {
310            return (
311                vec![Spans::from("\n\nthis is directory")],
312                "error".to_string(),
313            );
314        }
315        let cur_file_path = match file.entry.path().to_str() {
316            Some(p) => p,
317            None => "",
318        };
319        if cur_file_path == "" {
320            return (
321                vec![Spans::from("please press 'enter', select file")],
322                "error".to_string(),
323            );
324        }
325        let mut buf_new = String::new();
326        let err = File::open(cur_file_path)
327            .expect(&format!("file not found: {}", cur_file_path))
328            .read_to_string(&mut buf_new);
329        if err.is_err() {
330            return (
331                vec![Spans::from(format!(
332                    "open file:{}, error: {}",
333                    cur_file_path,
334                    err.err().unwrap()
335                ))],
336                "error".to_string(),
337            );
338        }
339
340        if file.state == crate::status::StatusItemType::Deleted
341            || file.state == crate::status::StatusItemType::New
342        {
343            let mut title = format!("Deleted: {}", cur_file_path);
344            let mut style = Color::Red;
345            if file.state == crate::status::StatusItemType::New {
346                title = format!("New File: {}", cur_file_path);
347                style = Color::Green;
348            }
349            let buf = io::BufReader::new(buf_new.as_bytes());
350            let contents: Vec<Spans> = buf
351                .lines()
352                .into_iter()
353                .map(|i| Spans::from(Span::styled(i.unwrap(), Style::default().fg(style))))
354                .collect();
355            return (contents, title);
356        }
357
358        let old_file_path = cur_file_path.replace(new_dir, old_dir);
359        let mut buf_old = String::new();
360        let err = File::open(&old_file_path)
361            .expect(&format!("file not found: {}", old_file_path))
362            .read_to_string(&mut buf_old);
363        if err.is_err() {
364            return (
365                vec![Spans::from(format!(
366                    "open file:{}, error: {}",
367                    old_file_path,
368                    err.err().unwrap()
369                ))],
370                "error".to_string(),
371            );
372        }
373
374        let diff = TextDiff::from_lines(&buf_old, &buf_new);
375        let contents: Vec<Spans> = diff
376            .iter_all_changes()
377            .into_iter()
378            .map(|i| {
379                let (sign, color) = match i.tag() {
380                    ChangeTag::Delete => ("-", Color::Red),
381                    ChangeTag::Insert => ("+", Color::Green),
382                    ChangeTag::Equal => (" ", Color::White),
383                };
384                Spans::from(Span::styled(
385                    format!("{} {}", sign, i),
386                    Style::default().fg(color),
387                ))
388            })
389            .collect();
390        let title = format!("Diff: {} and {}", cur_file_path, old_file_path);
391        (contents, title)
392    }
393
394    fn diff_list_dir(&mut self, progress: &mut impl FnMut(u16)) {
395        progress(10);
396        let old_dir = &self.old_dir;
397        let new_dir = &self.new_dir;
398        let old_files = list_dir(old_dir);
399        progress(20);
400        let new_files = list_dir(new_dir);
401        progress(30);
402        let mut res = Vec::new();
403
404        for (key, entry) in &old_files {
405            match new_files.get(key) {
406                None => {
407                    res.push(FolderStatefulList {
408                        entry: entry.clone(),
409                        state: crate::status::StatusItemType::Deleted,
410                    });
411                }
412                _ => {}
413            }
414        }
415        progress(40);
416
417        for (key, entry) in &new_files {
418            match old_files.get(key) {
419                None => {
420                    res.push(FolderStatefulList {
421                        entry: entry.clone(),
422                        state: crate::status::StatusItemType::New,
423                    });
424                }
425                Some(_) => {
426                    if entry.path().is_file() {
427                        let new_file_path = entry.path().canonicalize().unwrap();
428                        let old_file_path =
429                            new_file_path.to_str().unwrap().replace(new_dir, old_dir);
430                        let err = File::open(&old_file_path);
431                        match err {
432                            Ok(_) => {
433                                let is_same =
434                                    diff(new_file_path.to_str().unwrap(), old_file_path.as_str());
435                                if !is_same {
436                                    res.push(FolderStatefulList {
437                                        entry: entry.clone(),
438                                        state: crate::status::StatusItemType::Modified,
439                                    });
440                                }
441                                // * filter Normal
442                                // else {
443                                //     res.push(FolderStatefulList {
444                                //         entry: entry.clone(),
445                                //         state: crate::status::StatusItemType::Normal,
446                                //     });
447                                // }
448                            }
449                            _ => {}
450                        }
451                    }
452                }
453            }
454        }
455        progress(80);
456        delta_folder_stateful_list(&mut res);
457        self.items = StatefulList::with_items(res);
458        progress(100);
459    }
460}
461
462fn list_dir(path: &str) -> HashMap<String, DirEntry> {
463    let mut files = HashMap::new();
464    for f in walkdir::WalkDir::new(path) {
465        let entry = f.unwrap();
466        let key = entry
467            .path()
468            .canonicalize()
469            .unwrap()
470            .to_str()
471            .unwrap()
472            .replace(path, &"".to_string());
473        files.insert(key, entry);
474    }
475    files
476}
477
478fn delta_folder_stateful_list(files: &mut Vec<FolderStatefulList>) {
479    files.sort_by(|x, y| {
480        x.entry
481            .path()
482            .canonicalize()
483            .unwrap()
484            .to_str()
485            .unwrap()
486            .cmp(y.entry.path().canonicalize().unwrap().to_str().unwrap())
487    });
488    let mut i = 1;
489    while i < files.len() - 1 {
490        // same directory
491        if files[i - 1].entry.path().is_dir()
492            && (files[i - 1].state == crate::status::StatusItemType::Deleted
493                || files[i - 1].state == crate::status::StatusItemType::New)
494        {
495            if files[i]
496                .entry
497                .path()
498                .to_str()
499                .unwrap()
500                .starts_with(files[i - 1].entry.path().to_str().unwrap())
501            {
502                files.remove(i);
503                continue;
504            }
505        }
506        i += 1;
507    }
508}
509
510const MSG: [u8; 318] = [
511    84, 104, 105, 115, 32, 112, 114, 111, 106, 101, 99, 116, 32, 119, 97, 115, 32, 105, 110, 115,
512    112, 105, 114, 101, 100, 32, 98, 121, 32, 109, 121, 32, 103, 105, 114, 108, 102, 114, 105, 101,
513    110, 100, 44, 32, 119, 104, 111, 32, 114, 101, 113, 117, 101, 115, 116, 101, 100, 32, 97, 32,
514    116, 111, 111, 108, 32, 102, 111, 114, 32, 99, 111, 109, 112, 97, 114, 105, 110, 103, 32, 100,
515    105, 114, 101, 99, 116, 111, 114, 105, 101, 115, 59, 32, 97, 108, 116, 104, 111, 117, 103, 104,
516    32, 116, 104, 111, 117, 103, 104, 32, 86, 83, 32, 67, 111, 100, 101, 32, 97, 108, 114, 101, 97,
517    100, 121, 32, 111, 102, 102, 101, 114, 115, 32, 115, 117, 99, 104, 32, 97, 32, 112, 108, 117,
518    103, 45, 105, 110, 44, 32, 73, 32, 115, 116, 105, 108, 108, 32, 119, 97, 110, 116, 32, 116,
519    111, 32, 99, 114, 101, 97, 116, 101, 32, 111, 110, 101, 32, 102, 111, 114, 32, 104, 101, 114,
520    32, 40, 109, 111, 115, 116, 108, 121, 32, 115, 105, 110, 99, 101, 32, 73, 32, 100, 111, 110,
521    39, 116, 32, 104, 97, 118, 101, 32, 97, 110, 121, 32, 109, 111, 110, 101, 121, 32, 116, 111,
522    32, 112, 117, 114, 99, 104, 97, 115, 101, 32, 111, 116, 104, 101, 114, 32, 116, 104, 105, 110,
523    103, 115, 41, 59, 10, 73, 32, 119, 105, 115, 104, 32, 102, 111, 114, 32, 101, 118, 101, 114,
524    121, 111, 110, 101, 39, 115, 32, 104, 97, 112, 112, 105, 110, 101, 115, 115, 44, 32, 104, 101,
525    97, 108, 116, 104, 44, 32, 97, 110, 100, 32, 105, 110, 99, 114, 101, 97, 115, 105, 110, 103,
526    32, 119, 101, 97, 108, 116, 104, 59, 10, 50, 48, 50, 51, 48, 50, 49, 52,
527];