1use crate::{
2 app::{
3 app_helper::reset_card_drag_mode,
4 kanban::{Boards, Card, CardPriority, CardStatus},
5 state::{Focus, KeyBindingEnum},
6 App, DateTimeFormat,
7 },
8 constants::{
9 APP_TITLE, DEFAULT_BOARD_TITLE_LENGTH, DEFAULT_CARD_TITLE_LENGTH, FIELD_NOT_SET,
10 HIDDEN_PASSWORD_SYMBOL, LIST_SELECTED_SYMBOL, MOUSE_OUT_OF_BOUNDS_COORDINATES,
11 PATTERN_CHANGE_INTERVAL, SCROLLBAR_BEGIN_SYMBOL, SCROLLBAR_END_SYMBOL,
12 SCROLLBAR_TRACK_SYMBOL,
13 },
14 io::logger::{get_logs, get_selected_index, RUST_KANBAN_LOGGER},
15 ui::{
16 rendering::utils::{
17 centered_rect_with_length, check_for_card_drag_and_get_style,
18 check_if_active_and_get_style, check_if_mouse_is_in_area,
19 get_mouse_focusable_field_style,
20 },
21 theme::Theme,
22 },
23 util::{date_format_converter, date_format_finder},
24};
25use chrono::{Local, NaiveDate, NaiveDateTime};
26use log::Level;
27use ratatui::{
28 layout::{Alignment, Constraint, Direction, Layout, Margin, Rect},
29 style::{Color, Modifier, Style},
30 text::{Line, Span},
31 widgets::{
32 Block, BorderType, Borders, Cell, Gauge, Paragraph, Row, Scrollbar, ScrollbarOrientation,
33 ScrollbarState, Table,
34 },
35 Frame,
36};
37use std::{
38 cmp::Ordering,
39 time::{SystemTime, UNIX_EPOCH},
40};
41
42pub fn render_body(
43 rect: &mut Frame,
44 area: Rect,
45 app: &mut App,
46 preview_mode: bool,
47 is_active: bool,
48) {
49 let mut current_board_set = false;
50 let mut current_card_set = false;
51 let app_preview_boards_and_cards = app.preview_boards_and_cards.clone().unwrap_or_default();
52 let boards = if preview_mode {
53 if app_preview_boards_and_cards.is_empty() {
54 Boards::default()
55 } else {
56 app_preview_boards_and_cards
57 }
58 } else if !app.filtered_boards.is_empty() {
59 app.filtered_boards.clone()
60 } else {
61 app.boards.clone()
62 };
63 let scrollbar_style = check_for_card_drag_and_get_style(
64 app.state.card_drag_mode,
65 is_active,
66 app.current_theme.inactive_text_style,
67 app.current_theme.progress_bar_style,
68 );
69 let error_text_style = check_for_card_drag_and_get_style(
70 app.state.card_drag_mode,
71 is_active,
72 app.current_theme.inactive_text_style,
73 app.current_theme.error_text_style,
74 );
75 let general_style = check_for_card_drag_and_get_style(
76 app.state.card_drag_mode,
77 is_active,
78 app.current_theme.inactive_text_style,
79 app.current_theme.general_style,
80 );
81 let help_key_style = check_for_card_drag_and_get_style(
82 app.state.card_drag_mode,
83 is_active,
84 app.current_theme.inactive_text_style,
85 app.current_theme.help_key_style,
86 );
87 let current_board_id = &app.state.current_board_id.unwrap_or((0, 0));
88
89 let new_board_key = app
90 .get_first_keybinding(KeyBindingEnum::NewBoard)
91 .unwrap_or("".to_string());
92 let new_card_key = app
93 .get_first_keybinding(KeyBindingEnum::NewCard)
94 .unwrap_or("".to_string());
95
96 if preview_mode {
97 if app.preview_boards_and_cards.is_none()
98 || app
99 .preview_boards_and_cards
100 .as_ref()
101 .map_or(false, |v| v.is_empty())
102 {
103 let empty_paragraph = Paragraph::new("No boards found".to_string())
104 .alignment(Alignment::Center)
105 .block(
106 Block::default()
107 .title("Boards")
108 .borders(Borders::ALL)
109 .border_type(BorderType::Rounded),
110 )
111 .style(error_text_style);
112 rect.render_widget(empty_paragraph, area);
113 return;
114 }
115 } else if app.visible_boards_and_cards.is_empty() {
116 let empty_paragraph = Paragraph::new(
117 [
118 "No boards found, press ".to_string(),
119 new_board_key,
120 " to add a new board".to_string(),
121 ]
122 .concat(),
123 )
124 .alignment(Alignment::Center)
125 .block(
126 Block::default()
127 .title("Boards")
128 .borders(Borders::ALL)
129 .border_type(BorderType::Rounded),
130 )
131 .style(error_text_style);
132 rect.render_widget(empty_paragraph, area);
133 return;
134 }
135
136 let filter_chunks = if app.filtered_boards.is_empty() {
137 Layout::default()
138 .direction(Direction::Vertical)
139 .constraints([Constraint::Percentage(0), Constraint::Fill(1)].as_ref())
140 .split(area)
141 } else {
142 Layout::default()
143 .direction(Direction::Vertical)
144 .constraints([Constraint::Length(1), Constraint::Fill(1)].as_ref())
145 .split(area)
146 };
147
148 let chunks = if app.config.disable_scroll_bar {
149 Layout::default()
150 .direction(Direction::Vertical)
151 .constraints([Constraint::Fill(1)].as_ref())
152 .split(filter_chunks[1])
153 } else {
154 Layout::default()
155 .direction(Direction::Vertical)
156 .constraints([Constraint::Fill(1), Constraint::Length(1)].as_ref())
157 .split(filter_chunks[1])
158 };
159
160 if !app.filtered_boards.is_empty() {
161 let filtered_text = "This is a filtered view, Clear filter to see all boards and cards";
162 let filtered_paragraph = Paragraph::new(filtered_text.to_string())
163 .alignment(Alignment::Center)
164 .block(Block::default())
165 .style(error_text_style);
166 rect.render_widget(filtered_paragraph, filter_chunks[0]);
167 }
168
169 let mut constraints = vec![];
170 if boards.len() > app.config.no_of_boards_to_show.into() {
171 for _i in 0..app.config.no_of_boards_to_show {
172 constraints.push(Constraint::Fill(1));
173 }
174 } else {
175 for _i in 0..boards.len() {
176 constraints.push(Constraint::Fill(1));
177 }
178 }
179 let board_chunks = Layout::default()
180 .direction(Direction::Horizontal)
181 .constraints(AsRef::<[Constraint]>::as_ref(&constraints))
182 .split(chunks[0]);
183 let visible_boards_and_cards = if preview_mode {
184 app.state.preview_visible_boards_and_cards.clone()
185 } else {
186 app.visible_boards_and_cards.clone()
187 };
188 for (board_index, board_and_card_tuple) in visible_boards_and_cards.iter().enumerate() {
189 let board_id = board_and_card_tuple.0;
190 let board = boards.get_board_with_id(*board_id);
191 if board.is_none() {
192 continue;
193 }
194 let board = board.unwrap();
195 let board_title = board.name.clone();
196 let board_cards = board_and_card_tuple.1;
197 let board_title = if board_title.len() > DEFAULT_BOARD_TITLE_LENGTH.into() {
198 format!(
199 "{}...",
200 &board_title[0..DEFAULT_BOARD_TITLE_LENGTH as usize]
201 )
202 } else {
203 board_title
204 };
205 let board_title = format!("{} ({})", board_title, board.cards.len());
206 let board_title = if board_id == current_board_id {
207 format!("{} {}", ">>", board_title)
208 } else {
209 board_title
210 };
211
212 let mut card_constraints = vec![];
213 if board_cards.len() > app.config.no_of_cards_to_show.into() {
214 for _i in 0..app.config.no_of_cards_to_show {
215 card_constraints.push(Constraint::Fill(1));
216 }
217 } else if board_cards.is_empty() {
218 card_constraints.push(Constraint::Fill(1));
219 } else {
220 for _i in 0..board_cards.len() {
221 card_constraints.push(Constraint::Fill(1));
222 }
223 }
224
225 if board_index >= board_chunks.len() {
226 continue;
227 }
228
229 let board_style = check_for_card_drag_and_get_style(
230 app.state.card_drag_mode,
231 is_active,
232 app.current_theme.inactive_text_style,
233 app.current_theme.general_style,
234 );
235 let board_border_style = if !is_active {
237 app.current_theme.inactive_text_style
238 } else if check_if_mouse_is_in_area(
239 &app.state.current_mouse_coordinates,
240 &board_chunks[board_index],
241 ) {
242 app.state.mouse_focus = Some(Focus::Body);
243 app.state.set_focus(Focus::Body);
244 if !current_board_set {
245 app.state.current_board_id = Some(*board_id);
246 current_board_set = true;
247 }
248 app.state.hovered_board = Some(*board_id);
249 app.current_theme.mouse_focus_style
250 } else if (app.state.current_board_id.unwrap_or((0, 0)) == *board_id)
251 && app.state.current_card_id.is_none()
252 && matches!(app.state.focus, Focus::Body)
253 {
254 app.current_theme.keyboard_focus_style
255 } else if app.state.card_drag_mode {
256 app.current_theme.inactive_text_style
257 } else {
258 app.current_theme.general_style
259 };
260
261 let board_block = Block::default()
262 .title(&*board_title)
263 .borders(Borders::ALL)
264 .style(board_style)
265 .border_style(board_border_style)
266 .border_type(BorderType::Rounded);
267 rect.render_widget(board_block, board_chunks[board_index]);
268
269 let card_area_chunks = Layout::default()
270 .direction(Direction::Horizontal)
271 .constraints([Constraint::Fill(1)].as_ref())
272 .split(board_chunks[board_index]);
273
274 let card_chunks = Layout::default()
275 .direction(Direction::Vertical)
276 .margin(1)
277 .constraints(AsRef::<[Constraint]>::as_ref(&card_constraints))
278 .split(card_area_chunks[0]);
279 if board_cards.is_empty() {
280 let available_width = card_chunks[0].width - 2;
281 let empty_card_line = if preview_mode {
282 Line::from(Span::styled("No cards found", general_style))
283 } else {
284 Line::from(vec![
285 Span::styled("No cards found, press ", general_style),
286 Span::styled(&new_card_key, help_key_style),
287 Span::styled(" to add a new card", general_style),
288 ])
289 };
290 let empty_card_line_length = empty_card_line
291 .spans
292 .iter()
293 .fold(0, |acc, span| acc + span.content.chars().count());
294 let mut usable_length = empty_card_line_length as u16;
295 let mut usable_height = 1.0;
296 if empty_card_line_length > available_width.into() {
297 usable_length = available_width;
298 usable_height = empty_card_line_length as f32 / available_width as f32;
299 usable_height = usable_height.ceil();
300 }
301 let message_centered_rect =
302 centered_rect_with_length(usable_length, usable_height as u16, card_chunks[0]);
303 let empty_card_paragraph = Paragraph::new(empty_card_line)
304 .alignment(Alignment::Center)
305 .block(Block::default())
306 .style(board_style)
307 .wrap(ratatui::widgets::Wrap { trim: true });
308 rect.render_widget(empty_card_paragraph, message_centered_rect);
309 continue;
310 }
311 if !app.config.disable_scroll_bar && !board_cards.is_empty() && board_cards.len() > 1 {
312 let current_card_index = board
313 .cards
314 .get_card_index(app.state.current_card_id.unwrap_or((0, 0)))
315 .unwrap_or(0);
316 let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalLeft)
317 .begin_symbol(SCROLLBAR_BEGIN_SYMBOL)
318 .style(scrollbar_style)
319 .end_symbol(SCROLLBAR_END_SYMBOL)
320 .track_symbol(SCROLLBAR_TRACK_SYMBOL)
321 .track_style(app.current_theme.inactive_text_style);
322 let mut scrollbar_state = ScrollbarState::new(board.cards.len())
323 .position(current_card_index)
324 .viewport_content_length((card_chunks[0].height) as usize);
325 let scrollbar_area = card_area_chunks[0].inner(Margin {
326 vertical: 1,
327 horizontal: 0,
328 });
329 rect.render_stateful_widget(scrollbar, scrollbar_area, &mut scrollbar_state);
330 };
331 for (card_index, card_id) in board_cards.iter().enumerate() {
332 if app.state.hovered_card.is_some()
333 && app.state.card_drag_mode
334 && app.state.hovered_card.unwrap().1 == *card_id
335 {
336 continue;
337 }
338 let card = board.cards.get_card_with_id(*card_id);
339 if card.is_none() {
340 continue;
341 }
342 let card = card.unwrap();
343 let card_style = if !is_active {
345 app.current_theme.inactive_text_style
346 } else if check_if_mouse_is_in_area(
347 &app.state.current_mouse_coordinates,
348 &card_chunks[card_index],
349 ) {
350 app.state.mouse_focus = Some(Focus::Body);
351 app.state.set_focus(Focus::Body);
352 if !current_card_set {
353 app.state.current_card_id = Some(card.id);
354 current_card_set = true;
355 }
356 if !app.state.card_drag_mode {
357 app.state.hovered_card = Some((*board_id, card.id));
358 app.state.hovered_card_dimensions = Some((
359 card_chunks[card_index].width,
360 card_chunks[card_index].height,
361 ));
362 }
363 app.current_theme.mouse_focus_style
364 } else if app.state.current_card_id.unwrap_or((0, 0)) == card.id
365 && matches!(app.state.focus, Focus::Body)
366 && *board_id == *current_board_id
367 {
368 app.current_theme.keyboard_focus_style
369 } else if app.state.card_drag_mode {
370 app.current_theme.inactive_text_style
371 } else {
372 app.current_theme.general_style
373 };
374 render_a_single_card(
375 app,
376 card_chunks[card_index],
377 card_style,
378 card,
379 rect,
380 is_active,
381 );
382 }
383
384 if app.state.card_drag_mode {
385 }
387 }
388
389 if !app.config.disable_scroll_bar {
390 let current_board_index = boards.get_board_index(*current_board_id).unwrap_or(0) + 1;
391 let percentage = {
392 let temp_percent = (current_board_index as f64 / boards.len() as f64) * 100.0;
393 if temp_percent.is_nan() {
394 0
395 } else if temp_percent > 100.0 {
396 100
397 } else {
398 temp_percent as u16
399 }
400 };
401 let line_gauge = Gauge::default()
402 .block(Block::default())
403 .gauge_style(scrollbar_style)
404 .percent(percentage)
405 .label(format!("{} / {}", current_board_index, boards.len()));
406 rect.render_widget(line_gauge, chunks[1]);
407 }
408}
409
410pub fn render_card_being_dragged(
411 parent_body_area: Rect,
412 app: &mut App<'_>,
413 rect: &mut Frame<'_>,
414 is_active: bool,
415) {
416 if app.state.card_drag_mode {
417 if app.state.hovered_card.is_none() {
418 log::debug!("Hovered card is none");
419 return;
420 }
421 if app.state.hovered_card_dimensions.is_none() {
422 log::debug!("Hovered card dimensions are none");
423 return;
424 }
425
426 let current_mouse_coordinates = app.state.current_mouse_coordinates;
427 if current_mouse_coordinates == MOUSE_OUT_OF_BOUNDS_COORDINATES
428 || current_mouse_coordinates.0 < parent_body_area.x
429 || current_mouse_coordinates.1 < parent_body_area.y
430 || current_mouse_coordinates.0 > parent_body_area.x + parent_body_area.width
431 || current_mouse_coordinates.1 > parent_body_area.y + parent_body_area.height
432 {
433 log::debug!("Mouse is out of bounds");
434 reset_card_drag_mode(app);
435 return;
436 }
437 let card_dimensions = app.state.hovered_card_dimensions.unwrap();
438 let card_width = card_dimensions.0;
439 let card_height = card_dimensions.1;
440 let mut card_x = current_mouse_coordinates.0.saturating_sub(card_width / 2);
441 let mut card_y = current_mouse_coordinates.1.saturating_sub(card_height / 2);
442
443 if card_x < parent_body_area.x {
444 card_x = parent_body_area.x;
445 }
446 if card_y < parent_body_area.y {
447 card_y = parent_body_area.y;
448 }
449 if card_x + card_width > parent_body_area.x + parent_body_area.width {
450 card_x = parent_body_area.x + parent_body_area.width - card_width;
451 }
452 if card_y + card_height > parent_body_area.y + parent_body_area.height {
453 card_y = parent_body_area.y + parent_body_area.height - card_height;
454 }
455
456 let render_area = Rect::new(card_x, card_y, card_width, card_height);
457
458 let board_id = app.state.hovered_card.unwrap().0;
459 let card_id = app.state.hovered_card.unwrap().1;
460
461 let card = {
462 let board = app.boards.get_board_with_id(board_id);
463 if let Some(board) = board {
464 board.cards.get_card_with_id(card_id)
465 } else {
466 None
467 }
468 }
469 .cloned();
470
471 if card.is_none() {
472 log::debug!("Card is none");
473 return;
474 }
475 let card = card.unwrap();
476
477 render_blank_styled_canvas(rect, &app.current_theme, render_area, is_active);
478 render_a_single_card(
479 app,
480 render_area,
481 app.current_theme.error_text_style,
482 &card,
483 rect,
484 is_active,
485 )
486 }
487}
488
489pub fn render_close_button(rect: &mut Frame, app: &mut App, is_active: bool) {
490 let close_btn_area = Rect::new(rect.area().width - 3, 0, 3, 3);
491 let close_btn_style = if is_active
493 && check_if_mouse_is_in_area(&app.state.current_mouse_coordinates, &close_btn_area)
494 {
495 app.state.mouse_focus = Some(Focus::CloseButton);
496 app.state.set_focus(Focus::CloseButton);
497 let close_button_color = app.widgets.close_button.color;
498 let fg_color = app
499 .current_theme
500 .error_text_style
501 .fg
502 .unwrap_or(Color::White);
503 Style::default().fg(fg_color).bg(Color::Rgb(
504 close_button_color.0,
505 close_button_color.1,
506 close_button_color.2,
507 ))
508 } else if is_active {
509 app.current_theme.general_style
510 } else {
511 app.current_theme.inactive_text_style
512 };
513 let close_btn = Paragraph::new(vec![Line::from("X")])
514 .block(
515 Block::default()
516 .borders(Borders::ALL)
517 .border_type(BorderType::Rounded)
518 .style(close_btn_style),
519 )
520 .alignment(Alignment::Right);
521
522 render_blank_styled_canvas(rect, &app.current_theme, close_btn_area, is_active);
523 rect.render_widget(close_btn, close_btn_area);
524}
525
526pub fn render_blank_styled_canvas(
527 rect: &mut Frame,
528 current_theme: &Theme,
529 render_area: Rect,
530 is_active: bool,
531) {
532 let mut styled_text = String::with_capacity((render_area.width + 1) as usize);
534 for _ in 0..render_area.width + 1 {
535 styled_text.push(' ');
536 }
537 styled_text.push('\n');
538
539 let mut render_text =
540 String::with_capacity((render_area.height * (render_area.width + 1)) as usize);
541 for _ in 0..render_area.height {
542 render_text.push_str(&styled_text);
543 }
544
545 let styled_text = if is_active {
546 let mut style = current_theme.general_style;
547 style.add_modifier = Modifier::empty();
548 style.sub_modifier = Modifier::all();
549 Paragraph::new(render_text)
550 .style(style)
551 .block(Block::default())
552 } else {
553 let mut style = current_theme.inactive_text_style;
554 style.add_modifier = Modifier::empty();
555 style.sub_modifier = Modifier::all();
556 Paragraph::new(render_text)
557 .style(style)
558 .block(Block::default())
559 };
560 rect.render_widget(styled_text, render_area);
561}
562
563pub fn render_logs(
564 app: &mut App,
565 enable_focus_highlight: bool,
566 render_area: Rect,
567 rect: &mut Frame,
568 is_active: bool,
569) {
570 let log_box_border_style = if enable_focus_highlight {
571 get_mouse_focusable_field_style(app, Focus::Log, &render_area, is_active, false)
572 } else {
573 check_if_active_and_get_style(
574 is_active,
575 app.current_theme.inactive_text_style,
576 app.current_theme.general_style,
577 )
578 };
579 let date_format = app.config.date_time_format.to_parser_string();
580 let theme = &app.current_theme;
581 let all_logs = get_logs();
582 let mut highlight_style = check_if_active_and_get_style(
583 is_active,
584 theme.inactive_text_style,
585 theme.list_select_style,
586 );
587 let mut items = vec![];
588 let date_length = date_format.len() + 5;
589 let wrap_length = render_area.width as usize - date_length - 6; for log_record in all_logs.buffer {
591 let mut push_vec = vec![format!("[{}] - ", log_record.timestamp.format(date_format))];
592 let wrapped_text = textwrap::fill(&log_record.msg, wrap_length);
593 push_vec.push(wrapped_text);
594 push_vec.push(log_record.level.to_string());
595 items.push(push_vec);
596 }
597 let rows = items.iter().enumerate().map(|(index, item_og)| {
599 let mut item = item_og.clone();
600 let mut height = item
601 .iter()
602 .map(|content| content.chars().filter(|c| *c == '\n').count())
603 .max()
604 .unwrap_or(0)
605 + 1;
606 if height > (render_area.height as usize - 2) {
607 height = render_area.height as usize - 2;
608 }
609 let style = if !is_active {
610 theme.inactive_text_style
611 } else {
612 let style = match item[2].parse::<Level>().unwrap() {
613 Level::Error => theme.log_error_style,
614 Level::Warn => theme.log_warn_style,
615 Level::Info => theme.log_info_style,
616 Level::Debug => theme.log_debug_style,
617 Level::Trace => theme.log_trace_style,
618 };
619 if index == get_selected_index() {
620 highlight_style = style.add_modifier(Modifier::REVERSED);
621 };
622 style
623 };
624 item.remove(2);
625 let cells = item.iter().map(|c| Cell::from(c.to_string()).style(style));
626 Row::new(cells).height(height as u16)
627 });
628
629 let log_box_style = check_if_active_and_get_style(
630 is_active,
631 app.current_theme.inactive_text_style,
632 app.current_theme.general_style,
633 );
634
635 let log_list = Table::new(
636 rows,
637 [
638 Constraint::Length(date_length as u16),
639 Constraint::Length(wrap_length as u16),
640 ],
641 )
642 .block(
643 Block::default()
644 .title("Logs")
645 .style(log_box_style)
646 .border_style(log_box_border_style)
647 .borders(Borders::ALL)
648 .border_type(BorderType::Rounded),
649 )
650 .row_highlight_style(highlight_style)
651 .highlight_symbol(LIST_SELECTED_SYMBOL);
652
653 rect.render_stateful_widget(
654 log_list,
655 render_area,
656 &mut RUST_KANBAN_LOGGER.hot_log.lock().state,
657 );
658}
659
660fn render_a_single_card(
661 app: &mut App,
662 render_area: Rect,
663 card_style: Style,
664 card: &Card,
665 frame_to_render_on: &mut Frame,
666 is_active: bool,
667) {
668 let inner_card_chunks = Layout::default()
669 .direction(Direction::Vertical)
670 .constraints([Constraint::Fill(1), Constraint::Length(3)].as_ref())
671 .margin(1)
672 .split(render_area);
673
674 let card_title = if card.name.len() > DEFAULT_CARD_TITLE_LENGTH.into() {
675 format!("{}...", &card.name[0..DEFAULT_CARD_TITLE_LENGTH as usize])
676 } else {
677 card.name.clone()
678 };
679 let card_title = if app.state.current_card_id.unwrap_or((0, 0)) == card.id {
680 format!("{} {}", ">>", card_title)
681 } else {
682 card_title
683 };
684
685 let card_description = if card.description == FIELD_NOT_SET {
686 format!("Description: {}", FIELD_NOT_SET)
687 } else {
688 card.description.clone()
689 };
690
691 let card_due_default_style = check_if_active_and_get_style(
692 is_active,
693 app.current_theme.inactive_text_style,
694 app.current_theme.card_due_default_style,
695 );
696 let card_due_warning_style = check_if_active_and_get_style(
697 is_active,
698 app.current_theme.inactive_text_style,
699 app.current_theme.card_due_warning_style,
700 );
701 let card_due_overdue_style = check_if_active_and_get_style(
702 is_active,
703 app.current_theme.inactive_text_style,
704 app.current_theme.card_due_overdue_style,
705 );
706 let general_style = check_if_active_and_get_style(
707 is_active,
708 app.current_theme.inactive_text_style,
709 app.current_theme.general_style,
710 );
711
712 let mut card_extra_info = vec![Line::from("")];
713 if card.due_date == FIELD_NOT_SET {
714 card_extra_info.push(Line::from(Span::styled(
715 format!("Due: {}", FIELD_NOT_SET),
716 card_due_default_style,
717 )))
718 } else {
719 let card_due_date = card.due_date.clone();
720 let parsed_due_date =
721 date_format_converter(card_due_date.trim(), app.config.date_time_format);
722 let card_due_date_styled = if let Ok(parsed_due_date) = parsed_due_date {
723 if parsed_due_date == FIELD_NOT_SET || parsed_due_date.is_empty() {
724 Line::from(Span::styled(
725 format!("Due: {}", parsed_due_date),
726 card_due_default_style,
727 ))
728 } else {
729 let formatted_date_format = date_format_finder(&parsed_due_date).unwrap();
730 let (days_left, parsed_due_date) = match formatted_date_format {
731 DateTimeFormat::DayMonthYear
732 | DateTimeFormat::MonthDayYear
733 | DateTimeFormat::YearMonthDay => {
734 let today = Local::now().date_naive();
735 let string_to_naive_date_format = NaiveDate::parse_from_str(
736 &parsed_due_date,
737 app.config.date_time_format.to_parser_string(),
738 )
739 .unwrap();
740 let days_left = string_to_naive_date_format
741 .signed_duration_since(today)
742 .num_days();
743 let parsed_due_date = string_to_naive_date_format
744 .format(app.config.date_time_format.to_parser_string())
745 .to_string();
746 (days_left, parsed_due_date)
747 }
748 DateTimeFormat::DayMonthYearTime
749 | DateTimeFormat::MonthDayYearTime
750 | DateTimeFormat::YearMonthDayTime {} => {
751 let today = Local::now().naive_local();
752 let string_to_naive_date_format = NaiveDateTime::parse_from_str(
753 &parsed_due_date,
754 app.config.date_time_format.to_parser_string(),
755 )
756 .unwrap();
757 let days_left = string_to_naive_date_format
758 .signed_duration_since(today)
759 .num_days();
760 let parsed_due_date = string_to_naive_date_format
761 .format(app.config.date_time_format.to_parser_string())
762 .to_string();
763 (days_left, parsed_due_date)
764 }
765 };
766 if days_left >= 0 {
767 match days_left.cmp(&(app.config.warning_delta as i64)) {
768 Ordering::Less | Ordering::Equal => Line::from(Span::styled(
769 format!("Due: {}", parsed_due_date),
770 card_due_warning_style,
771 )),
772 Ordering::Greater => Line::from(Span::styled(
773 format!("Due: {}", parsed_due_date),
774 card_due_default_style,
775 )),
776 }
777 } else {
778 Line::from(Span::styled(
779 format!("Due: {}", parsed_due_date),
780 card_due_overdue_style,
781 ))
782 }
783 }
784 } else {
785 Line::from(Span::styled(
786 format!("Due: {}", card_due_date),
787 card_due_default_style,
788 ))
789 };
790 card_extra_info.extend(vec![card_due_date_styled]);
791 }
792
793 let mut card_status = format!("Status: {}", card.card_status.clone());
794 let mut card_priority = format!("Priority: {}", card.priority.clone());
795 let required_space = card_status.len() + 3 + card_priority.len(); if required_space > (render_area.width - 2) as usize {
799 card_status = format!("S: {}", card.card_status.clone());
801 card_priority = format!("P: {}", card.priority.clone());
802 }
803 let spacer_span = Span::styled(" | ", general_style);
804 let card_status = if !is_active {
805 Span::styled(card_status, app.current_theme.inactive_text_style)
806 } else {
807 match card.card_status {
808 CardStatus::Active => {
809 Span::styled(card_status, app.current_theme.card_status_active_style)
810 }
811 CardStatus::Complete => {
812 Span::styled(card_status, app.current_theme.card_status_completed_style)
813 }
814 CardStatus::Stale => {
815 Span::styled(card_status, app.current_theme.card_status_stale_style)
816 }
817 }
818 };
819 let card_priority = if !is_active {
820 Span::styled(card_priority, app.current_theme.inactive_text_style)
821 } else {
822 match card.priority {
823 CardPriority::High => {
824 Span::styled(card_priority, app.current_theme.card_priority_high_style)
825 }
826 CardPriority::Medium => {
827 Span::styled(card_priority, app.current_theme.card_priority_medium_style)
828 }
829 CardPriority::Low => {
830 Span::styled(card_priority, app.current_theme.card_priority_low_style)
831 }
832 }
833 };
834 let status_line = Line::from(vec![card_priority, spacer_span, card_status]);
835 card_extra_info.extend(vec![status_line]);
836
837 let card_block = Block::default()
838 .title(&*card_title)
839 .borders(Borders::ALL)
840 .border_style(card_style)
841 .border_type(BorderType::Rounded);
842 let card_paragraph = Paragraph::new(card_description)
843 .alignment(Alignment::Left)
844 .block(Block::default())
845 .wrap(ratatui::widgets::Wrap { trim: false });
846 let card_extra_info = Paragraph::new(card_extra_info)
847 .alignment(Alignment::Left)
848 .block(Block::default())
849 .wrap(ratatui::widgets::Wrap { trim: false });
850
851 frame_to_render_on.render_widget(card_block, render_area);
852 frame_to_render_on.render_widget(card_paragraph, inner_card_chunks[0]);
853 frame_to_render_on.render_widget(card_extra_info, inner_card_chunks[1]);
854}
855
856pub fn draw_title<'a>(app: &mut App, render_area: Rect, is_active: bool) -> Paragraph<'a> {
857 let title_style = check_if_active_and_get_style(
858 is_active,
859 app.current_theme.inactive_text_style,
860 app.current_theme.general_style,
861 );
862 let border_style =
863 get_mouse_focusable_field_style(app, Focus::Title, &render_area, is_active, false);
864 Paragraph::new(APP_TITLE)
865 .alignment(Alignment::Center)
866 .block(
867 Block::default()
868 .style(title_style)
869 .borders(Borders::ALL)
870 .border_style(border_style)
871 .border_type(BorderType::Rounded),
872 )
873}
874
875pub fn draw_help<'a>(
876 app: &mut App,
877 render_area: Rect,
878 is_active: bool,
879) -> (Block<'a>, Table<'a>, Table<'a>) {
880 let border_style =
881 get_mouse_focusable_field_style(app, Focus::Help, &render_area, is_active, false);
882 let help_key_style = check_if_active_and_get_style(
883 is_active,
884 app.current_theme.inactive_text_style,
885 app.current_theme.help_key_style,
886 );
887 let help_text_style = check_if_active_and_get_style(
888 is_active,
889 app.current_theme.inactive_text_style,
890 app.current_theme.help_text_style,
891 );
892 let current_element_style = check_if_active_and_get_style(
893 is_active,
894 app.current_theme.inactive_text_style,
895 app.current_theme.list_select_style,
896 );
897
898 let rows: Vec<Row> = app
899 .config
900 .keybindings
901 .iter()
902 .map(|item| {
903 let keys = item
904 .1
905 .iter()
906 .map(|key| key.to_string())
907 .collect::<Vec<String>>()
908 .join(", ");
909 let cells = vec![
910 Cell::from(item.0.to_string()).style(help_text_style),
911 Cell::from(keys).style(help_key_style),
912 ];
913 Row::new(cells)
914 })
915 .collect();
916
917 let mid_point = rows.len() / 2;
918 let left_rows = rows[..mid_point].to_vec();
919 let right_rows = rows[mid_point..].to_vec();
920
921 let left_table = Table::new(
922 left_rows,
923 [Constraint::Percentage(70), Constraint::Percentage(30)],
924 )
925 .block(Block::default().style(help_text_style))
926 .row_highlight_style(current_element_style)
927 .highlight_symbol(">> ")
928 .style(border_style);
929
930 let right_table = Table::new(
931 right_rows,
932 [Constraint::Percentage(70), Constraint::Percentage(30)],
933 )
934 .block(Block::default().style(help_text_style))
935 .row_highlight_style(current_element_style)
936 .highlight_symbol(">> ")
937 .style(border_style);
938
939 let border_block = Block::default()
940 .title("Help")
941 .borders(Borders::ALL)
942 .style(help_text_style)
943 .border_style(border_style)
944 .border_type(BorderType::Rounded);
945
946 (border_block, left_table, right_table)
947}
948
949pub fn draw_crab_pattern(
951 render_area: Rect,
952 style: Style,
953 is_active: bool,
954 disable_animations: bool,
955) -> Paragraph<'static> {
956 let crab_pattern = if !is_active || disable_animations {
957 create_crab_pattern_1(render_area.width, render_area.height, is_active)
958 } else {
959 let patterns = [
960 create_crab_pattern_1(render_area.width, render_area.height, is_active),
961 create_crab_pattern_2(render_area.width, render_area.height, is_active),
962 create_crab_pattern_3(render_area.width, render_area.height, is_active),
963 ];
964 let index = (get_time_offset() / PATTERN_CHANGE_INTERVAL) as usize % patterns.len();
966 patterns[index].clone()
967 };
968 Paragraph::new(crab_pattern)
969 .style(style)
970 .block(Block::default())
971}
972
973fn create_crab_pattern_1(width: u16, height: u16, is_active: bool) -> String {
974 let mut pattern = String::new();
975 for row in 0..height {
976 for col in 0..width {
977 if (row + col) % 2 == 0 {
978 if is_active {
979 pattern.push('🦀');
980 } else {
981 pattern.push_str(HIDDEN_PASSWORD_SYMBOL.to_string().as_str());
982 }
983 } else {
984 pattern.push_str(" ");
985 }
986 }
987 pattern.push('\n');
988 }
989 pattern
990}
991
992fn create_crab_pattern_2(width: u16, height: u16, is_active: bool) -> String {
993 let mut pattern = String::new();
994 let block_size = 4;
995
996 for row in 0..height {
997 let block_row = row % block_size;
998
999 for col in 0..width {
1000 let block_col = col % block_size;
1001
1002 if (block_row == 0 && block_col <= 1)
1003 || (block_row == 1 && block_col >= 2)
1004 || (block_row == 2 && block_col <= 1)
1005 {
1006 if is_active {
1007 pattern.push_str(" 🦀 ");
1008 } else {
1009 pattern.push_str(HIDDEN_PASSWORD_SYMBOL.to_string().as_str());
1010 }
1011 } else {
1012 pattern.push_str(" ");
1013 }
1014 }
1015 pattern.push('\n');
1016 }
1017 pattern
1018}
1019
1020fn create_crab_pattern_3(width: u16, height: u16, is_active: bool) -> String {
1021 let mut pattern = String::new();
1022 for row in 0..height {
1023 for col in 0..width {
1024 if (row % 2 == 0 && col % 2 == 0) || (row % 2 == 1 && col % 2 == 1) {
1025 if is_active {
1026 pattern.push_str(" 🦀 ");
1027 } else {
1028 pattern.push_str(HIDDEN_PASSWORD_SYMBOL.to_string().as_str());
1029 }
1030 } else {
1031 pattern.push_str(" ");
1032 }
1033 }
1034 pattern.push('\n');
1035 }
1036 pattern
1037}
1038
1039fn get_time_offset() -> u64 {
1040 let start_time = SystemTime::now();
1041 let since_epoch = start_time.duration_since(UNIX_EPOCH).unwrap();
1042 since_epoch.as_millis() as u64
1043}
1044
1045pub fn render_blank_styled_canvas_with_margin(
1046 rect: &mut Frame,
1047 app: &mut App,
1048 render_area: Rect,
1049 is_active: bool,
1050 margin: i16,
1051) {
1052 let general_style = check_if_active_and_get_style(
1053 is_active,
1054 app.current_theme.inactive_text_style,
1055 app.current_theme.general_style,
1056 );
1057
1058 let x = render_area.x as i16 + margin;
1059 let x = if x < 0 { 0 } else { x };
1060 let y = render_area.y as i16 + margin;
1061 let y = if y < 0 { 0 } else { y };
1062 let width = render_area.width as i16 - margin * 2;
1063 let width = if width < 0 { 0 } else { width };
1064 let height = render_area.height as i16 - margin * 2;
1065 let height = if height < 0 { 0 } else { height };
1066
1067 let new_render_area = Rect::new(x as u16, y as u16, width as u16, height as u16);
1068
1069 let mut styled_text = vec![];
1070 for _ in 0..new_render_area.width + 1 {
1071 styled_text.push(" ".to_string());
1072 }
1073 let mut render_text = vec![];
1074 for _ in 0..new_render_area.height {
1075 render_text.push(format!("{}\n", styled_text.join("")));
1076 }
1077 let styled_text = Paragraph::new(render_text.join(""))
1078 .style(general_style)
1079 .block(Block::default());
1080 rect.render_widget(styled_text, new_render_area);
1081}