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 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 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 }); });
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 }
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 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];