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}