rust_kanban/ui/rendering/popup/
edit_general_config.rs

1use crate::{
2    app::{
3        state::{AppStatus, Focus, KeyBindingEnum},
4        App, ConfigEnum,
5    },
6    ui::{
7        rendering::{
8            common::{render_blank_styled_canvas, render_close_button, render_logs},
9            popup::EditGeneralConfig,
10            utils::{
11                calculate_viewport_corrected_cursor_position, centered_rect_with_percentage,
12                check_if_active_and_get_style, check_if_mouse_is_in_area,
13                get_mouse_focusable_field_style,
14            },
15        },
16        Renderable,
17    },
18};
19use ratatui::{
20    layout::{Alignment, Constraint, Direction, Layout},
21    text::{Line, Span},
22    widgets::{Block, BorderType, Borders, Paragraph},
23    Frame,
24};
25use std::str::FromStr;
26
27impl Renderable for EditGeneralConfig {
28    fn render(rect: &mut Frame, app: &mut App, is_active: bool) {
29        let area = centered_rect_with_percentage(70, 70, rect.area());
30
31        let chunks = if app.config.enable_mouse_support {
32            Layout::default()
33                .direction(Direction::Vertical)
34                .constraints(
35                    [
36                        Constraint::Length(6),
37                        Constraint::Fill(1),
38                        Constraint::Length(5),
39                        Constraint::Length(3),
40                    ]
41                    .as_ref(),
42                )
43                .split(area)
44        } else {
45            Layout::default()
46                .direction(Direction::Vertical)
47                .constraints(
48                    [
49                        Constraint::Length(6),
50                        Constraint::Fill(1),
51                        Constraint::Length(4),
52                    ]
53                    .as_ref(),
54                )
55                .split(area)
56        };
57
58        let edit_box_style = get_mouse_focusable_field_style(
59            app,
60            Focus::EditGeneralConfigPopup,
61            &chunks[1],
62            is_active,
63            true,
64        );
65        let help_text_style = check_if_active_and_get_style(
66            is_active,
67            app.current_theme.inactive_text_style,
68            app.current_theme.help_text_style,
69        );
70        let help_key_style = check_if_active_and_get_style(
71            is_active,
72            app.current_theme.inactive_text_style,
73            app.current_theme.help_key_style,
74        );
75        let general_style = check_if_active_and_get_style(
76            is_active,
77            app.current_theme.inactive_text_style,
78            app.current_theme.general_style,
79        );
80        let error_text_style = check_if_active_and_get_style(
81            is_active,
82            app.current_theme.inactive_text_style,
83            app.current_theme.error_text_style,
84        );
85        let card_status_active_style = check_if_active_and_get_style(
86            is_active,
87            app.current_theme.inactive_text_style,
88            app.current_theme.card_status_active_style,
89        );
90        let keyboard_focus_style = check_if_active_and_get_style(
91            is_active,
92            app.current_theme.inactive_text_style,
93            app.current_theme.keyboard_focus_style,
94        );
95        let mouse_focus_style = check_if_active_and_get_style(
96            is_active,
97            app.current_theme.inactive_text_style,
98            app.current_theme.mouse_focus_style,
99        );
100
101        let list_items = app.config.to_view_list();
102        let config_item_name = if let Some(index) = app.state.app_table_states.config.selected() {
103            list_items[index].first().unwrap()
104        } else {
105            // NOTE: This is temporary, as only the Theme editor uses this other than config
106            "Theme Name"
107        };
108        if let Ok(config_enum) = ConfigEnum::from_str(config_item_name) {
109            app.state.path_check_state.path_check_mode = config_enum == ConfigEnum::SaveDirectory;
110        }
111        let config_item_value = if app.state.app_table_states.config.selected().is_some() {
112            list_items
113                .iter()
114                .find(|x| x.first().unwrap() == config_item_name)
115                .unwrap()
116                .get(1)
117                .unwrap()
118                .to_owned()
119        } else {
120            app.state.theme_being_edited.name.clone()
121        };
122
123        let accept_key = app
124            .get_first_keybinding(KeyBindingEnum::Accept)
125            .unwrap_or("".to_string());
126        let start_editing_key = app
127            .get_first_keybinding(KeyBindingEnum::TakeUserInput)
128            .unwrap_or("".to_string());
129        let cancel_key = app
130            .get_first_keybinding(KeyBindingEnum::GoToPreviousViewOrCancel)
131            .unwrap_or("".to_string());
132        let stop_editing_key = app
133            .get_first_keybinding(KeyBindingEnum::StopUserInput)
134            .unwrap_or("".to_string());
135
136        let paragraph_text = vec![
137            Line::from(vec![
138                Span::styled("Current Value is '", help_text_style),
139                Span::styled(config_item_value, help_key_style),
140                Span::styled("'", help_text_style),
141            ]),
142            Line::from(String::from("")),
143            Line::from(vec![
144                Span::styled("Press ", help_text_style),
145                Span::styled(start_editing_key, help_key_style),
146                Span::styled(" to edit, or ", help_text_style),
147                Span::styled(cancel_key, help_key_style),
148                Span::styled(" to cancel, Press ", help_text_style),
149                Span::styled(stop_editing_key, help_key_style),
150                Span::styled(" to stop editing and press ", help_text_style),
151                Span::styled(accept_key, help_key_style),
152                Span::styled(" on Submit to save", help_text_style),
153            ]),
154        ];
155        let paragraph_title = Line::from(vec![Span::raw(config_item_name)]);
156        let config_item = Paragraph::new(paragraph_text)
157            .block(
158                Block::default()
159                    .title(paragraph_title)
160                    .style(general_style)
161                    .borders(Borders::ALL)
162                    .border_type(BorderType::Rounded),
163            )
164            .wrap(ratatui::widgets::Wrap { trim: true });
165        let current_user_input = app.state.text_buffers.general_config.get_joined_lines();
166        let user_input = if app.state.path_check_state.path_check_mode {
167            if (current_user_input != app.state.path_check_state.path_last_checked)
168                || app.state.path_check_state.recheck_required
169            {
170                app.state.path_check_state.recheck_required = false;
171                app.state.path_check_state.potential_completion = None;
172                app.state
173                    .path_check_state
174                    .path_last_checked
175                    .clone_from(&current_user_input);
176                app.state.path_check_state.path_exists =
177                    std::path::Path::new(&current_user_input).is_dir();
178                if !app.state.path_check_state.path_exists {
179                    let mut split_input = current_user_input
180                        .split(std::path::MAIN_SEPARATOR)
181                        .collect::<Vec<&str>>();
182                    // remove any empty strings
183                    split_input.retain(|&x| !x.is_empty());
184                    if !split_input.is_empty() {
185                        let last_input = split_input.pop().unwrap();
186                        if split_input.is_empty() {
187                            let to_check = last_input;
188                            let dir = std::fs::read_dir(std::path::MAIN_SEPARATOR.to_string());
189                            if dir.is_ok() {
190                                let dir = dir.unwrap();
191                                // only retain the ones that are directories
192                                let dir = dir.flatten();
193                                for entry in dir {
194                                    let path = entry.path();
195                                    if path.to_str().unwrap().starts_with(
196                                        &(std::path::MAIN_SEPARATOR.to_string() + to_check),
197                                    ) && path.is_dir()
198                                    {
199                                        app.state.path_check_state.potential_completion = Some(
200                                            path.to_str()
201                                                .unwrap()
202                                                .to_string()
203                                                .strip_prefix(
204                                                    &(std::path::MAIN_SEPARATOR.to_string()
205                                                        + to_check),
206                                                )
207                                                .unwrap()
208                                                .to_string(),
209                                        );
210                                        break;
211                                    }
212                                }
213                            }
214                        } else {
215                            let to_check = std::path::MAIN_SEPARATOR.to_string()
216                                + &split_input.join(std::path::MAIN_SEPARATOR_STR);
217                            if std::path::Path::new(&to_check).is_dir() {
218                                let dir = std::fs::read_dir(&to_check);
219                                let dir = dir.unwrap();
220                                // only retain the ones that are directories
221                                let dir = dir.flatten();
222                                for entry in dir {
223                                    let path = entry.path();
224                                    if path
225                                        .to_str()
226                                        .unwrap()
227                                        .starts_with(current_user_input.as_str())
228                                        && path.is_dir()
229                                    {
230                                        app.state.path_check_state.potential_completion = Some(
231                                            path.to_str()
232                                                .unwrap()
233                                                .strip_prefix(current_user_input.as_str())
234                                                .unwrap()
235                                                .to_string(),
236                                        );
237                                        break;
238                                    }
239                                }
240                            }
241                        };
242                    }
243                }
244            }
245            if !current_user_input.is_empty() {
246                if let Some(potential_completion) = &app.state.path_check_state.potential_completion
247                {
248                    Line::from(vec![
249                        Span::styled(current_user_input.clone(), general_style),
250                        Span::styled(
251                            potential_completion.clone(),
252                            app.current_theme.inactive_text_style,
253                        ),
254                        Span::styled(
255                            " (Press 'Tab' or 'Right Arrow' to autocomplete)",
256                            help_text_style,
257                        ),
258                    ])
259                } else if app.state.path_check_state.path_exists {
260                    Line::from(Span::styled(
261                        current_user_input.clone(),
262                        card_status_active_style,
263                    ))
264                } else {
265                    Line::from(vec![
266                        Span::styled(
267                            current_user_input.clone(),
268                            error_text_style,
269                        ),
270                        Span::styled(
271                            " (Path does not exist) - Press '%' to create a new directory at this location",
272                            help_text_style,
273                        ),
274                    ])
275                }
276            } else {
277                Line::from(Span::styled(
278                    "No input",
279                    app.current_theme.inactive_text_style,
280                ))
281            }
282        } else {
283            Line::from(Span::styled(current_user_input, general_style))
284        };
285        let edit_item = Paragraph::new(user_input)
286            .block(
287                Block::default()
288                    .title("Edit")
289                    .style(general_style)
290                    .borders(Borders::ALL)
291                    .border_style(edit_box_style)
292                    .border_type(BorderType::Rounded),
293            )
294            .wrap(ratatui::widgets::Wrap { trim: true });
295
296        let clear_area = centered_rect_with_percentage(80, 80, rect.area());
297        let clear_area_border = Block::default()
298            .title("Config Editor")
299            .style(general_style)
300            .borders(Borders::ALL)
301            .border_style(keyboard_focus_style)
302            .border_type(BorderType::Rounded);
303
304        render_blank_styled_canvas(rect, &app.current_theme, clear_area, is_active);
305        rect.render_widget(clear_area_border, clear_area);
306        rect.render_widget(config_item, chunks[0]);
307        rect.render_widget(edit_item, chunks[1]);
308        render_logs(app, false, chunks[2], rect, is_active);
309
310        if app.config.enable_mouse_support {
311            let submit_button_style =
312                if check_if_mouse_is_in_area(&app.state.current_mouse_coordinates, &chunks[3]) {
313                    app.state.mouse_focus = Some(Focus::SubmitButton);
314                    app.state.set_focus(Focus::SubmitButton);
315                    mouse_focus_style
316                } else if app.state.app_status == AppStatus::KeyBindMode {
317                    keyboard_focus_style
318                } else {
319                    general_style
320                };
321            let submit_button = Paragraph::new("Submit")
322                .block(
323                    Block::default()
324                        .style(general_style)
325                        .borders(Borders::ALL)
326                        .border_style(submit_button_style)
327                        .border_type(BorderType::Rounded),
328                )
329                .alignment(Alignment::Center);
330            rect.render_widget(submit_button, chunks[3]);
331            render_close_button(rect, app, is_active)
332        }
333
334        if app.state.app_status == AppStatus::UserInput {
335            let (x_pos, y_pos) = calculate_viewport_corrected_cursor_position(
336                &app.state.text_buffers.general_config,
337                &app.config.show_line_numbers,
338                &chunks[1],
339            );
340            rect.set_cursor_position((x_pos, y_pos));
341        }
342    }
343}