git_function_history_gui/
lib.rs

1use std::{sync::mpsc, time::Duration};
2
3use eframe::{
4    self,
5    egui::{self, Button, Layout, Sense, SidePanel},
6    epaint::Vec2,
7};
8use eframe::{
9    egui::{Label, TextEdit, TopBottomPanel, Visuals},
10    epaint::Color32,
11};
12use function_history_backend_thread::types::{
13    Command, CommandResult, FilterType, FullCommand, HistoryFilterType, ListType, Status,
14};
15use git_function_history::{
16    types::Directions, BlockType, CommitFunctions, FileType, Filter, FunctionHistory,
17};
18
19// TODO: stop cloning everyting and use references instead
20pub struct MyEguiApp {
21    command: Command,
22    dark_theme: bool,
23    input_buffer: String,
24    cmd_output: CommandResult,
25    status: Status,
26    list_type: ListType,
27    channels: (
28        mpsc::Sender<FullCommand>,
29        mpsc::Receiver<(CommandResult, Status)>,
30    ),
31    filter: Filter,
32    file_type: FileType,
33    history_filter_type: HistoryFilterType,
34}
35
36impl MyEguiApp {
37    pub fn new(
38        _cc: &eframe::CreationContext<'_>,
39        channels: (
40            mpsc::Sender<FullCommand>,
41            mpsc::Receiver<(CommandResult, Status)>,
42        ),
43    ) -> Self {
44        Self {
45            dark_theme: true,
46            command: Command::Search,
47            input_buffer: String::new(),
48            cmd_output: CommandResult::None,
49            status: Status::default(),
50            list_type: ListType::default(),
51            channels,
52            file_type: FileType::None,
53            filter: Filter::None,
54            history_filter_type: HistoryFilterType::None,
55        }
56    }
57
58    fn draw_commit(commit: &mut CommitFunctions, ctx: &egui::Context, show: bool) {
59        if show {
60            TopBottomPanel::top("date_id").show(ctx, |ui| {
61                ui.add(Label::new(format!(
62                    "Commit: {}",
63                    commit.get_metadata()["commit hash"]
64                )));
65                ui.add(Label::new(format!(
66                    "Date: {}",
67                    commit.get_metadata()["date"]
68                )));
69            });
70        }
71        TopBottomPanel::top("file_name").show(ctx, |ui| {
72            ui.add(Label::new(format!(
73                "File {}",
74                commit.get_metadata()["file"]
75            )));
76        });
77        match commit.get_move_direction() {
78            Directions::None => {
79                egui::CentralPanel::default().show(ctx, |ui| {
80                    egui::ScrollArea::vertical()
81                        .max_height(f32::INFINITY)
82                        .max_width(f32::INFINITY)
83                        .auto_shrink([false, false])
84                        .show(ui, |ui| {
85                            ui.add(Label::new(commit.get_file().to_string()));
86                        });
87                });
88            }
89            Directions::Forward => {
90                // split the screen in two parts, most of it is for the content, the and leave a small part for the right arrow
91                log::debug!("found at least one file index beginning");
92                let resp = egui::SidePanel::right("right_arrow")
93                    .show(ctx, |ui| {
94                        ui.set_width(0.5);
95                        ui.add_sized(
96                            Vec2::new(ui.available_width(), ui.available_height()),
97                            Button::new("->"),
98                        )
99                    })
100                    .inner;
101                egui::CentralPanel::default().show(ctx, |ui| {
102                    egui::ScrollArea::vertical()
103                        .max_height(f32::INFINITY)
104                        .max_width(f32::INFINITY)
105                        .auto_shrink([false, false])
106                        .show(ui, |ui| ui.add(Label::new(commit.get_file().to_string())));
107                });
108                if resp.clicked() {
109                    commit.move_forward();
110                }
111            }
112            Directions::Back => {
113                log::debug!("found at least one file index end");
114                // split the screen in two parts, leave a small part for the left arrow and the rest for the content
115                let resp = SidePanel::left("right_button")
116                    .show(ctx, |ui| {
117                        ui.set_width(1.0);
118                        ui.add_sized(
119                            Vec2::new(ui.available_width(), ui.available_height()),
120                            Button::new("<-"),
121                        )
122                    })
123                    .inner;
124                egui::CentralPanel::default().show(ctx, |ui| {
125                    egui::ScrollArea::vertical()
126                        .max_height(f32::INFINITY)
127                        .max_width(f32::INFINITY)
128                        .auto_shrink([false, false])
129                        .show(ui, |ui| {
130                            ui.add(Label::new(commit.get_file().to_string()));
131                        });
132                });
133                if resp.clicked() {
134                    commit.move_back();
135                }
136            }
137            Directions::Both => {
138                log::debug!("found at least one file index middle");
139                // split screen into 3 parts, leave a small part for the left arrow, the middle part for the content and leave a small part for the right arrow
140                let l_resp = SidePanel::left("left_arrow")
141                    .show(ctx, |ui| {
142                        ui.set_width(1.0);
143                        ui.add_sized(
144                            Vec2::new(ui.available_width(), ui.available_height()),
145                            Button::new("<-"),
146                        )
147                    })
148                    .inner;
149                let r_resp = egui::SidePanel::right("right_arrows")
150                    .show(ctx, |ui| {
151                        ui.set_width(1.0);
152                        ui.add_sized(
153                            Vec2::new(ui.available_width(), ui.available_height()),
154                            Button::new("->"),
155                        )
156                    })
157                    .inner;
158                egui::CentralPanel::default().show(ctx, |ui| {
159                    egui::ScrollArea::vertical()
160                        .max_height(f32::INFINITY)
161                        .max_width(f32::INFINITY)
162                        .auto_shrink([false, false])
163                        .show(ui, |ui| {
164                            ui.add(Label::new(commit.get_file().to_string()));
165                        });
166                });
167                if l_resp.clicked() {
168                    commit.move_back();
169                } else if r_resp.clicked() {
170                    commit.move_forward();
171                }
172            }
173        }
174    }
175
176    fn draw_history(history: &mut FunctionHistory, ctx: &egui::Context) {
177        // split the screen top and bottom into two parts, leave small part for the left arrow commit hash and right arrow and the rest for the content
178        // create a 3 line header
179        TopBottomPanel::top("control history").show(ctx, |ui| {
180            ui.set_height(2.0);
181            ui.horizontal(|ui| {
182                let mut max = ui.available_width();
183                let l_resp = match history.get_move_direction() {
184                    Directions::Forward => {
185                        ui.add_sized(Vec2::new(2.0, 2.0), Button::new("<-").sense(Sense::hover()));
186                        None
187                    }
188                    _ => Some(
189                        // add a left arrow button that is disabled
190                        ui.add_sized(Vec2::new(2.0, 2.0), Button::new("<-")),
191                    ),
192                };
193                max -= ui.available_width();
194                ui.add_sized(
195                    Vec2::new(ui.available_width() - max, 2.0),
196                    Label::new(format!(
197                        "{}\n{}",
198                        history.get_metadata()["commit hash"],
199                        history.get_metadata()["date"]
200                    )),
201                );
202
203                let r_resp = match history.get_move_direction() {
204                    Directions::Back => {
205                        ui.add_sized(Vec2::new(2.0, 2.0), Button::new("->").sense(Sense::hover()));
206                        None
207                    }
208                    _ => {
209                        // add a right arrow button that is disabled
210                        Some(ui.add_sized(Vec2::new(2.0, 2.0), Button::new("->")))
211                    }
212                };
213
214                if let Some(r_resp) = r_resp {
215                    if r_resp.clicked() {
216                        history.move_forward();
217                    }
218                }
219                if let Some(l_resp) = l_resp {
220                    if l_resp.clicked() {
221                        history.move_back();
222                    }
223                }
224            });
225        });
226        Self::draw_commit(history.get_mut_commit(), ctx, false);
227    }
228}
229
230impl eframe::App for MyEguiApp {
231    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
232        ctx.request_repaint();
233        if self.dark_theme {
234            ctx.set_visuals(Visuals::dark());
235        } else {
236            ctx.set_visuals(Visuals::light());
237        }
238        egui::TopBottomPanel::bottom("status_bar").show(ctx, |ui| {
239            ui.add_space(20.);
240            egui::menu::bar(ui, |ui| {
241                ui.with_layout(
242                    Layout::left_to_right(eframe::emath::Align::Center),
243                    |ui| match &self.status {
244                        Status::Loading => {
245                            ui.colored_label(Color32::BLUE, "Loading...");
246                        }
247                        Status::Ok(a) => match a {
248                            Some(a) => {
249                                ui.colored_label(Color32::LIGHT_GREEN, format!("Ok: {}", a));
250                            }
251                            None => {
252                                ui.colored_label(Color32::GREEN, "Ready");
253                            }
254                        },
255                        Status::Warning(a) => {
256                            ui.colored_label(Color32::LIGHT_RED, format!("Warn: {}", a));
257                        }
258                        Status::Error(a) => {
259                            ui.colored_label(Color32::LIGHT_RED, format!("Error: {}", a));
260                        }
261                    },
262                );
263                // controls
264                ui.with_layout(Layout::right_to_left(eframe::emath::Align::Center), |ui| {
265                    let theme_btn = ui.add(Button::new({
266                        if self.dark_theme {
267                            "🌞"
268                        } else {
269                            "🌙"
270                        }
271                    }));
272                    if theme_btn.clicked() {
273                        self.dark_theme = !self.dark_theme;
274                    }
275                });
276            });
277
278            ui.add_space(20.);
279        });
280        egui::TopBottomPanel::bottom("commnad_builder").show(ctx, |ui| {
281            egui::menu::bar(ui, |ui| {
282                let max = ui.available_width() / 6.0;
283                egui::ComboBox::from_id_source("command_combo_box")
284                    .selected_text(self.command.to_string())
285                    .show_ui(ui, |ui| {
286                        ui.selectable_value(&mut self.command, Command::Filter, "filter");
287                        ui.selectable_value(&mut self.command, Command::Search, "search");
288                        ui.selectable_value(&mut self.command, Command::List, "list");
289                    });
290                match self.command {
291                    Command::Filter => {
292                        match &self.cmd_output {
293                            CommandResult::History(_) => {
294                                // Options 1. by date 2. by commit hash 3. in date range 4. function in block 5. function in lines 6. function in function
295                                let text = match &self.history_filter_type {
296                                    HistoryFilterType::None => "filter type".to_string(),
297                                    a => a.to_string(),
298                                };
299                                egui::ComboBox::from_id_source("history_combo_box")
300                                    .selected_text(text)
301                                    .show_ui(ui, |ui| {
302                                        ui.selectable_value(
303                                            &mut self.history_filter_type,
304                                            HistoryFilterType::Date(String::new()),
305                                            "by date",
306                                        );
307                                        ui.selectable_value(
308                                            &mut self.history_filter_type,
309                                            HistoryFilterType::CommitHash(String::new()),
310                                            "by commit hash",
311                                        );
312                                        ui.selectable_value(
313                                            &mut self.history_filter_type,
314                                            HistoryFilterType::DateRange(
315                                                String::new(),
316                                                String::new(),
317                                            ),
318                                            "in date range",
319                                        );
320                                        ui.selectable_value(
321                                            &mut self.history_filter_type,
322                                            HistoryFilterType::FunctionInBlock(String::new()),
323                                            "function in block",
324                                        );
325                                        ui.selectable_value(
326                                            &mut self.history_filter_type,
327                                            HistoryFilterType::FunctionInLines(
328                                                String::new(),
329                                                String::new(),
330                                            ),
331                                            "function in lines",
332                                        );
333                                        ui.selectable_value(
334                                            &mut self.history_filter_type,
335                                            HistoryFilterType::FunctionInFunction(String::new()),
336                                            "function in function",
337                                        );
338                                        ui.selectable_value(
339                                            &mut self.history_filter_type,
340                                            HistoryFilterType::FileAbsolute(String::new()),
341                                            "file absolute",
342                                        );
343                                        ui.selectable_value(
344                                            &mut self.history_filter_type,
345                                            HistoryFilterType::FileRelative(String::new()),
346                                            "file relative",
347                                        );
348                                        ui.selectable_value(
349                                            &mut self.history_filter_type,
350                                            HistoryFilterType::Directory(String::new()),
351                                            "directory",
352                                        );
353                                        ui.selectable_value(
354                                            &mut self.history_filter_type,
355                                            HistoryFilterType::None,
356                                            "none",
357                                        );
358                                    });
359                                match &mut self.history_filter_type {
360                                    HistoryFilterType::DateRange(line1, line2)
361                                    | HistoryFilterType::FunctionInLines(line1, line2) => {
362                                        ui.horizontal(|ui| {
363                                            // set the width of the input field
364                                            ui.set_min_width(4.0);
365                                            ui.set_max_width(max);
366                                            ui.add(TextEdit::singleline(line1));
367                                        });
368                                        ui.horizontal(|ui| {
369                                            // set the width of the input field
370                                            ui.set_min_width(4.0);
371                                            ui.set_max_width(max);
372                                            ui.add(TextEdit::singleline(line2));
373                                        });
374                                    }
375                                    HistoryFilterType::Date(dir)
376                                    | HistoryFilterType::CommitHash(dir)
377                                    | HistoryFilterType::FunctionInBlock(dir)
378                                    | HistoryFilterType::FunctionInFunction(dir)
379                                    | HistoryFilterType::FileAbsolute(dir)
380                                    | HistoryFilterType::FileRelative(dir)
381                                    | HistoryFilterType::Directory(dir) => {
382                                        ui.horizontal(|ui| {
383                                            // set the width of the input field
384                                            ui.set_min_width(4.0);
385                                            ui.set_max_width(max);
386                                            ui.add(TextEdit::singleline(dir));
387                                        });
388                                    }
389                                    HistoryFilterType::None => {
390                                        // do nothing
391                                    }
392                                }
393                                let resp = ui.add(Button::new("Go"));
394                                if resp.clicked() {
395                                    self.status = Status::Loading;
396                                    let filter = match &self.history_filter_type {
397                                        HistoryFilterType::Date(date) => {
398                                            Some(Filter::Date(date.to_string()))
399                                        }
400                                        HistoryFilterType::CommitHash(commit_hash) => {
401                                            Some(Filter::CommitHash(commit_hash.to_string()))
402                                        }
403                                        HistoryFilterType::DateRange(date1, date2) => Some(
404                                            Filter::DateRange(date1.to_string(), date2.to_string()),
405                                        ),
406                                        HistoryFilterType::FunctionInBlock(block) => Some(
407                                            Filter::FunctionInBlock(BlockType::from_string(block)),
408                                        ),
409                                        HistoryFilterType::FunctionInLines(line1, line2) => {
410                                            let fn_in_lines = (
411                                                match line1.parse::<usize>() {
412                                                    Ok(x) => x,
413                                                    Err(e) => {
414                                                        self.status =
415                                                            Status::Error(format!("{}", e));
416                                                        return;
417                                                    }
418                                                },
419                                                match line2.parse::<usize>() {
420                                                    Ok(x) => x,
421                                                    Err(e) => {
422                                                        self.status =
423                                                            Status::Error(format!("{}", e));
424                                                        return;
425                                                    }
426                                                },
427                                            );
428                                            Some(Filter::FunctionInLines(
429                                                fn_in_lines.0,
430                                                fn_in_lines.1,
431                                            ))
432                                        }
433                                        HistoryFilterType::FunctionInFunction(function) => {
434                                            Some(Filter::FunctionWithParent(function.to_string()))
435                                        }
436                                        HistoryFilterType::FileAbsolute(file) => {
437                                            Some(Filter::FileAbsolute(file.to_string()))
438                                        }
439                                        HistoryFilterType::FileRelative(file) => {
440                                            Some(Filter::FileRelative(file.to_string()))
441                                        }
442                                        HistoryFilterType::Directory(dir) => {
443                                            Some(Filter::Directory(dir.to_string()))
444                                        }
445                                        HistoryFilterType::None => {
446                                            self.status = Status::Ok(None);
447                                            None
448                                        }
449                                    };
450                                    if let Some(filter) = filter {
451                                        self.channels
452                                            .0
453                                            .send(FullCommand::Filter(FilterType {
454                                                thing: self.cmd_output.clone(),
455                                                filter,
456                                            }))
457                                            .unwrap();
458                                    }
459                                }
460                            }
461
462                            _ => {
463                                ui.add(Label::new("No filters available"));
464                            }
465                        }
466                    }
467                    Command::Search => {
468                        ui.add(Label::new("Function Name:"));
469                        ui.horizontal(|ui| {
470                            // set the width of the input field
471                            ui.set_min_width(4.0);
472                            ui.set_max_width(max);
473                            ui.add(TextEdit::singleline(&mut self.input_buffer));
474                        });
475
476                        let text = match &self.file_type {
477                            FileType::Directory(_) => "directory",
478                            FileType::Absolute(_) => "absolute",
479                            FileType::Relative(_) => "relative",
480                            _ => "file type",
481                        };
482                        egui::ComboBox::from_id_source("search_file_combo_box")
483                            .selected_text(text)
484                            .show_ui(ui, |ui| {
485                                ui.selectable_value(&mut self.file_type, FileType::None, "None");
486                                ui.selectable_value(
487                                    &mut self.file_type,
488                                    FileType::Relative(String::new()),
489                                    "Relative",
490                                );
491                                ui.selectable_value(
492                                    &mut self.file_type,
493                                    FileType::Absolute(String::new()),
494                                    "Absolute",
495                                );
496                                ui.selectable_value(
497                                    &mut self.file_type,
498                                    FileType::Directory(String::new()),
499                                    "Directory",
500                                );
501                            });
502                        match &mut self.file_type {
503                            FileType::None => {}
504                            FileType::Relative(dir)
505                            | FileType::Absolute(dir)
506                            | FileType::Directory(dir) => {
507                                ui.horizontal(|ui| {
508                                    // set the width of the input field
509                                    ui.set_min_width(4.0);
510                                    ui.set_max_width(max);
511                                    ui.add(TextEdit::singleline(dir));
512                                });
513                            }
514                        }
515                        // get filters if any
516                        let text = match &self.filter {
517                            Filter::CommitHash(_) => "commit hash".to_string(),
518                            Filter::DateRange(..) => "date range".to_string(),
519                            Filter::Date(_) => "date".to_string(),
520                            _ => "filter type".to_string(),
521                        };
522                        egui::ComboBox::from_id_source("search_search_filter_combo_box")
523                            .selected_text(text)
524                            .show_ui(ui, |ui| {
525                                ui.selectable_value(&mut self.filter, Filter::None, "None");
526                                ui.selectable_value(
527                                    &mut self.filter,
528                                    Filter::CommitHash(String::new()),
529                                    "Commit Hash",
530                                );
531                                ui.selectable_value(
532                                    &mut self.filter,
533                                    Filter::Date(String::new()),
534                                    "Date",
535                                );
536                                ui.selectable_value(
537                                    &mut self.filter,
538                                    Filter::DateRange(String::new(), String::new()),
539                                    "Date Range",
540                                );
541                            });
542                        match &mut self.filter {
543                            Filter::None => {}
544                            Filter::CommitHash(thing) | Filter::Date(thing) => {
545                                ui.horizontal(|ui| {
546                                    // set the width of the input field
547                                    ui.set_min_width(4.0);
548                                    ui.set_max_width(max);
549                                    ui.add(TextEdit::singleline(thing));
550                                });
551                            }
552                            Filter::DateRange(start, end) => {
553                                ui.horizontal(|ui| {
554                                    // set the width of the input field
555                                    ui.set_min_width(4.0);
556                                    ui.set_max_width(max);
557                                    ui.add(TextEdit::singleline(start));
558                                });
559                                ui.add(Label::new("-"));
560                                ui.horizontal(|ui| {
561                                    // set the width of the input field
562                                    ui.set_min_width(4.0);
563                                    ui.set_max_width(max);
564                                    ui.add(TextEdit::singleline(end));
565                                });
566                            }
567                            _ => {}
568                        }
569                        let resp = ui.add(Button::new("Go"));
570                        if resp.clicked() {
571                            self.status = Status::Loading;
572                            self.channels
573                                .0
574                                .send(FullCommand::Search(
575                                    self.input_buffer.clone(),
576                                    self.file_type.clone(),
577                                    self.filter.clone(),
578                                ))
579                                .unwrap();
580                        }
581                    }
582                    Command::List => {
583                        egui::ComboBox::from_id_source("list_type")
584                            .selected_text(self.list_type.to_string())
585                            .show_ui(ui, |ui| {
586                                ui.selectable_value(&mut self.list_type, ListType::Dates, "dates");
587                                ui.selectable_value(
588                                    &mut self.list_type,
589                                    ListType::Commits,
590                                    "commits",
591                                );
592                            });
593                        let resp = ui.add(Button::new("Go"));
594                        if resp.clicked() {
595                            self.status = Status::Loading;
596                            self.channels
597                                .0
598                                .send(FullCommand::List(self.list_type))
599                                .unwrap();
600                        }
601                    }
602                }
603            });
604        });
605
606        egui::CentralPanel::default().show(ctx, |ui| {
607            // check if the channel has a message and if so set it to self.command
608            match self.channels.1.recv_timeout(Duration::from_millis(100)) {
609                Ok(timeout) => match timeout {
610                    (_, Status::Error(e)) => {
611                        let e = e.split_once("why").unwrap_or((&e, ""));
612                        let e = format!(
613                            "error recieved last command didn't work; {}{}",
614                            e.0,
615                            e.1.split_once("why").unwrap_or(("", "")).0,
616                        );
617                        log::warn!("{}", e);
618                        self.status = Status::Error(e);
619                    }
620                    (t, Status::Ok(msg)) => {
621                        log::info!("got results of last command");
622                        self.status = Status::Ok(msg);
623                        self.cmd_output = t;
624                    }
625                    _ => {}
626                },
627                Err(e) => match e {
628                    mpsc::RecvTimeoutError::Timeout => {}
629                    mpsc::RecvTimeoutError::Disconnected => {
630                        panic!("Disconnected");
631                    }
632                },
633            }
634            // match self.commmand and render based on that
635            match &mut self.cmd_output {
636                CommandResult::History(t) => {
637                    Self::draw_history(t, ctx);
638                }
639
640                CommandResult::String(t) => {
641                    egui::ScrollArea::vertical()
642                        .max_height(f32::INFINITY)
643                        .max_width(f32::INFINITY)
644                        .auto_shrink([false, false])
645                        .show(ui, |ui| {
646                            for line in t {
647                                if !line.is_empty() {
648                                    ui.add(Label::new(line.to_string()));
649                                }
650                            }
651                        });
652                }
653                CommandResult::None => match &self.status {
654                    Status::Loading => {
655                        ui.add(Label::new("Loading..."));
656                    }
657                    _ => {
658                        ui.add(Label::new("Nothing to show"));
659                        ui.add(Label::new("Please select a command"));
660                    }
661                },
662            };
663        });
664    }
665}