Skip to main content

endpoint_validator/tui/
ui.rs

1use crate::tui::state::{AppBlock, AppState, EndpointField, JsonViewMode, SettingsField};
2use crate::tui::widgets::{
3    create_button, create_input_widget, create_json_viewer, create_list_widget,
4};
5use ratatui::{
6    Frame,
7    backend::Backend,
8    layout::{Constraint, Direction, Layout, Rect},
9    style::{Color, Style},
10    text::{Span, Spans, Text},
11    widgets::{Block, Borders, Paragraph},
12};
13
14pub fn draw_ui<B: Backend>(f: &mut Frame<B>, app_state: &mut AppState) {
15    let chunks = Layout::default()
16        .direction(Direction::Vertical)
17        .constraints(
18            [
19                Constraint::Length(5),
20                Constraint::Min(1),
21                Constraint::Length(2),
22            ]
23            .as_ref(),
24        )
25        .split(f.size());
26
27    draw_settings_screen(f, app_state, chunks[0]);
28
29    let main_chunks = Layout::default()
30        .direction(Direction::Horizontal)
31        .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
32        .split(chunks[1]);
33
34    draw_endpoints_screen(f, app_state, main_chunks[0]);
35    draw_response_screen(f, app_state, main_chunks[1]);
36
37    draw_help_text(f, chunks[2]);
38}
39
40fn draw_help_text<B: Backend>(f: &mut Frame<B>, area: Rect) {
41    let help_text = Paragraph::new("Press Esc to quit | Use Tab key to switch block | Use arrow keys to navigate | Enter to press button")
42        .style(Style::default().fg(Color::Gray))
43        .alignment(ratatui::layout::Alignment::Center);
44    f.render_widget(help_text, area);
45}
46
47fn draw_settings_screen<B: Backend>(f: &mut Frame<B>, app_state: &mut AppState, area: Rect) {
48    let is_focused = app_state.current_block == AppBlock::Settings;
49    let title = Spans::from(vec![Span::styled(
50        " Settings ",
51        Style::default().fg(if is_focused {
52            Color::Yellow
53        } else {
54            Color::Gray
55        }),
56    )]);
57
58    let settings_block = Block::default()
59        .borders(Borders::ALL)
60        .border_style(Style::default().fg(if is_focused {
61            Color::Yellow
62        } else {
63            Color::Gray
64        }))
65        .title(title);
66
67    f.render_widget(settings_block, area);
68
69    let settings_chunks = Layout::default()
70        .direction(Direction::Horizontal)
71        .constraints([
72            Constraint::Percentage(20),
73            Constraint::Percentage(20),
74            Constraint::Percentage(20),
75            Constraint::Percentage(20),
76            Constraint::Percentage(20),
77        ])
78        .margin(1)
79        .split(area);
80
81    let url_input = create_input_widget(
82        " URL ",
83        &app_state.url,
84        app_state.focused_settings_field == Some(SettingsField::Url),
85    );
86    let username_input = create_input_widget(
87        " Username ",
88        &app_state.username,
89        app_state.focused_settings_field == Some(SettingsField::Username),
90    );
91    let password_input = create_input_widget(
92        " Password ",
93        &app_state.password,
94        app_state.focused_settings_field == Some(SettingsField::Password),
95    );
96
97    let connect_button = create_button(
98        "Connect",
99        !app_state.connected,
100        app_state.focused_settings_field == Some(SettingsField::ConnectButton),
101    );
102
103    let disconnect_button = create_button(
104        "Disconnect",
105        app_state.connected,
106        app_state.focused_settings_field == Some(SettingsField::DisconnectButton),
107    );
108
109    f.render_widget(url_input, settings_chunks[0]);
110    f.render_widget(username_input, settings_chunks[1]);
111    f.render_widget(password_input, settings_chunks[2]);
112    f.render_widget(connect_button, settings_chunks[3]);
113    f.render_widget(disconnect_button, settings_chunks[4]);
114}
115
116fn draw_endpoints_screen<B: Backend>(f: &mut Frame<B>, app_state: &mut AppState, area: Rect) {
117    let endpoint_chunks = Layout::default()
118        .direction(Direction::Horizontal)
119        .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
120        .split(area);
121
122    let is_focused = app_state.current_block == AppBlock::EndpointList;
123    if app_state.connected {
124        let list_widget = create_list_widget(
125            &app_state.endpoints,
126            app_state.selected_endpoint,
127            is_focused,
128        );
129        f.render_widget(list_widget, endpoint_chunks[0]);
130    } else {
131        let title = Spans::from(vec![Span::styled(
132            " Endpoint List ",
133            Style::default().fg(if is_focused {
134                Color::Yellow
135            } else {
136                Color::Gray
137            }),
138        )]);
139
140        let empty_block = Block::default()
141            .borders(Borders::ALL)
142            .border_style(Style::default().fg(if is_focused {
143                Color::Yellow
144            } else {
145                Color::Gray
146            }))
147            .title(title);
148
149        f.render_widget(empty_block, endpoint_chunks[0]);
150    }
151
152    let is_focused = app_state.current_block == AppBlock::EndpointsReq;
153
154    let title = Spans::from(vec![Span::styled(
155        " Request ",
156        Style::default().fg(if is_focused {
157            Color::Yellow
158        } else {
159            Color::Gray
160        }),
161    )]);
162
163    let request_block = Block::default()
164        .borders(Borders::ALL)
165        .border_style(Style::default().fg(if is_focused {
166            Color::Yellow
167        } else {
168            Color::Gray
169        }))
170        .title(title);
171
172    f.render_widget(request_block, endpoint_chunks[1]);
173
174    let num_params = app_state.params.len();
175    let num_items = num_params + 4;
176
177    let mut constraints = Vec::new();
178
179    for _ in 0..num_items {
180        constraints.push(Constraint::Length(3));
181    }
182
183    constraints.push(Constraint::Min(1));
184
185    let request_chunks = Layout::default()
186        .direction(Direction::Vertical)
187        .constraints(constraints.as_slice())
188        .margin(1)
189        .split(endpoint_chunks[1]);
190
191    let service_name_text = match &app_state.service_name {
192        Some(service_name) => service_name.clone(),
193        None => String::from(""),
194    };
195
196    let service_name_paragraph = Paragraph::new(Text::raw(service_name_text))
197        .block(
198            Block::default()
199                .borders(Borders::ALL)
200                .border_style(Style::default().fg(Color::Gray))
201                .title(" Service Name "),
202        )
203        .style(Style::default().fg(Color::Gray))
204        .alignment(ratatui::layout::Alignment::Left);
205    f.render_widget(service_name_paragraph, request_chunks[0]);
206
207    let method_id_text = match app_state.method_id {
208        Some(method_id) => method_id.to_string(),
209        None => String::from(""),
210    };
211
212    let method_id_paragraph = Paragraph::new(Text::raw(method_id_text))
213        .block(
214            Block::default()
215                .borders(Borders::ALL)
216                .border_style(Style::default().fg(Color::Gray))
217                .title(" Method ID "),
218        )
219        .style(Style::default().fg(Color::Gray))
220        .alignment(ratatui::layout::Alignment::Left);
221    f.render_widget(method_id_paragraph, request_chunks[1]);
222
223    for (i, param) in app_state.params.iter().enumerate() {
224        let param_label = format!(" {}:{:?} ", param.name, param.ty);
225        let param_value = app_state.param_values[i].to_string();
226        let param_input = create_input_widget(
227            &param_label,
228            &param_value,
229            app_state.focused_endpoint_field == Some(EndpointField::Param(i)),
230        );
231        f.render_widget(param_input, request_chunks[i + 2]);
232    }
233
234    // Layout for Connect and Disconnect buttons
235    let button_chunks = Layout::default()
236        .direction(Direction::Horizontal)
237        .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
238        .split(request_chunks[request_chunks.len() - 3]);
239
240    let connect_button = create_button(
241        "Connect",
242        !app_state.endpoint_connected,
243        app_state.focused_endpoint_field == Some(EndpointField::ConnectButton),
244    );
245
246    let disconnect_button = create_button(
247        "Disconnect",
248        app_state.endpoint_connected,
249        app_state.focused_endpoint_field == Some(EndpointField::DisconnectButton),
250    );
251
252    f.render_widget(connect_button, button_chunks[0]);
253    f.render_widget(disconnect_button, button_chunks[1]);
254
255    let json_toggle_button = create_button(
256        if app_state.json_view_mode == JsonViewMode::Pretty {
257            " Pretty JSON "
258        } else {
259            " Raw JSON "
260        },
261        true,
262        app_state.focused_endpoint_field == Some(EndpointField::JsonToggleButton),
263    );
264
265    f.render_widget(json_toggle_button, request_chunks[request_chunks.len() - 2]);
266}
267
268fn draw_response_screen<B: Backend>(f: &mut Frame<B>, app_state: &mut AppState, area: Rect) {
269    let is_focused = app_state.current_block == AppBlock::EndpointsRes;
270    let json_viewer = create_json_viewer(&app_state.json_data, is_focused)
271        .scroll((app_state.response_scroll.0, app_state.response_scroll.1));
272    f.render_widget(json_viewer, area);
273}