Skip to content

Commit d97476c

Browse files
committedMar 6, 2024··
feat: ⚡ added tags and tag filtering
changed focused pane border style
1 parent 8bf24d5 commit d97476c

File tree

9 files changed

+194
-29
lines changed

9 files changed

+194
-29
lines changed
 

‎src/pages/home.rs

+18-7
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,29 @@ pub struct State {
2222
pub openapi_path: String,
2323
pub openapi_spec: Spec,
2424
pub active_operation_index: usize,
25+
pub active_tag_name: Option<String>,
2526
}
2627

2728
impl State {
2829
pub fn active_operation(&self) -> Option<(String, String, &Operation)> {
29-
if let Some((path, method, operation)) = self.openapi_spec.operations().nth(self.active_operation_index) {
30-
Some((path, method.to_string(), operation))
31-
} else {
32-
None
30+
if let Some(active_tag) = &self.active_tag_name {
31+
if let Some((path, method, operation)) =
32+
self.openapi_spec.operations().filter(|item| item.2.tags.contains(active_tag)).nth(self.active_operation_index)
33+
{
34+
return Some((path, method.to_string(), operation));
35+
}
36+
} else if let Some((path, method, operation)) = self.openapi_spec.operations().nth(self.active_operation_index) {
37+
return Some((path, method.to_string(), operation));
3338
}
39+
None
3440
}
3541

3642
pub fn operations_len(&self) -> usize {
37-
self.openapi_spec.operations().count()
43+
if let Some(active_tag) = &self.active_tag_name {
44+
self.openapi_spec.operations().filter(|item| item.2.tags.contains(active_tag)).count()
45+
} else {
46+
self.openapi_spec.operations().count()
47+
}
3848
}
3949
}
4050

@@ -51,7 +61,8 @@ pub struct Home {
5161
impl Home {
5262
pub fn new(openapi_path: String) -> Result<Self> {
5363
let openapi_spec = oas3::from_path(openapi_path.clone())?;
54-
let state = Arc::new(RwLock::new(State { openapi_spec, openapi_path, active_operation_index: 0 }));
64+
let state =
65+
Arc::new(RwLock::new(State { openapi_spec, openapi_path, active_operation_index: 0, active_tag_name: None }));
5566
let focused_border_style = Style::default().fg(Color::LightGreen);
5667

5768
Ok(Self {
@@ -60,7 +71,7 @@ impl Home {
6071
panes: vec![
6172
Box::new(ProfilesPane::new(false, focused_border_style)),
6273
Box::new(ApisPane::new(state.clone(), true, focused_border_style)),
63-
Box::new(TagsPane::new(false, focused_border_style)),
74+
Box::new(TagsPane::new(state.clone(), false, focused_border_style)),
6475
Box::new(AddressPane::new(state.clone(), false, focused_border_style)),
6576
Box::new(RequestPane::new(false, focused_border_style)),
6677
Box::new(ResponsePane::new(false, focused_border_style)),

‎src/panes/address.rs

+17-2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ impl AddressPane {
3333
}
3434
}
3535

36+
fn border_type(&self) -> BorderType {
37+
match self.focused {
38+
true => BorderType::Thick,
39+
false => BorderType::Plain,
40+
}
41+
}
42+
3643
fn method_color(method: &str) -> Color {
3744
match method {
3845
"GET" => Color::LightCyan,
@@ -86,7 +93,6 @@ impl Pane for AddressPane {
8693
};
8794
let title = operation.summary.clone().unwrap_or_default();
8895
let inner_margin = Margin { horizontal: 2, vertical: 2 };
89-
frame.render_widget(Block::default().title(title).borders(Borders::ALL).border_style(self.border_style()), area);
9096
let inner = area.inner(&inner_margin);
9197
frame.render_widget(
9298
Paragraph::new(Line::from(vec![
@@ -95,7 +101,16 @@ impl Pane for AddressPane {
95101
Span::styled(path, Style::default().fg(Color::White)),
96102
])),
97103
inner,
98-
)
104+
);
105+
106+
frame.render_widget(
107+
Block::default()
108+
.title(title)
109+
.borders(Borders::ALL)
110+
.border_style(self.border_style())
111+
.border_type(self.border_type()),
112+
area,
113+
);
99114
}
100115

101116
Ok(())

‎src/panes/apis.rs

+31-9
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ pub struct ApisPane {
2323

2424
impl ApisPane {
2525
pub fn new(state: Arc<RwLock<State>>, focused: bool, focused_border_style: Style) -> Self {
26-
Self { focused, focused_border_style, state: state.clone(), current_operation_index: 0 }
26+
Self { focused, focused_border_style, state, current_operation_index: 0 }
2727
}
2828

2929
fn border_style(&self) -> Style {
@@ -33,6 +33,13 @@ impl ApisPane {
3333
}
3434
}
3535

36+
fn border_type(&self) -> BorderType {
37+
match self.focused {
38+
true => BorderType::Thick,
39+
false => BorderType::Plain,
40+
}
41+
}
42+
3643
fn method_color(method: &str) -> Color {
3744
match method {
3845
"GET" => Color::LightCyan,
@@ -89,6 +96,10 @@ impl Pane for ApisPane {
8996
state.active_operation_index = self.current_operation_index;
9097
return Ok(Some(Action::Update));
9198
},
99+
Action::Update => {
100+
let state = self.state.read().unwrap();
101+
self.current_operation_index = state.active_operation_index;
102+
},
92103
_ => {},
93104
}
94105

@@ -98,8 +109,13 @@ impl Pane for ApisPane {
98109
fn draw(&mut self, frame: &mut Frame<'_>, area: Rect) -> Result<()> {
99110
let state = self.state.read().unwrap();
100111
let unknown = String::from("Unknown");
101-
let items = state.openapi_spec.operations().map(|operation| {
102-
Line::from(vec![
112+
let items = state.openapi_spec.operations().filter_map(|operation| {
113+
if let Some(active_tag) = &state.active_tag_name {
114+
if !operation.2.tags.contains(active_tag) {
115+
return None;
116+
}
117+
}
118+
Some(Line::from(vec![
103119
Span::styled(
104120
format!("{:7}", operation.1.as_str()),
105121
Style::default().fg(Self::method_color(operation.1.as_str())),
@@ -108,7 +124,7 @@ impl Pane for ApisPane {
108124
operation.2.summary.as_ref().unwrap_or(operation.2.operation_id.as_ref().unwrap_or(&unknown)),
109125
Style::default().fg(Color::White),
110126
),
111-
])
127+
]))
112128
});
113129

114130
let list = List::new(items)
@@ -118,12 +134,18 @@ impl Pane for ApisPane {
118134
let mut list_state = ListState::default().with_selected(Some(self.current_operation_index));
119135

120136
frame.render_stateful_widget(list, area, &mut list_state);
121-
137+
let active_tag = format!("[{}]", state.active_tag_name.clone().unwrap_or(String::from("ALL")));
122138
frame.render_widget(
123-
Block::default().title("APIs").borders(Borders::ALL).border_style(self.border_style()).title_bottom(
124-
Line::from(format!("{} of {}", self.current_operation_index.saturating_add(1), state.operations_len()))
125-
.right_aligned(),
126-
),
139+
Block::default()
140+
.title("APIs")
141+
.borders(Borders::ALL)
142+
.border_style(self.border_style())
143+
.border_type(self.border_type())
144+
.title_bottom(
145+
Line::from(format!("{} of {}", self.current_operation_index.saturating_add(1), state.operations_len()))
146+
.right_aligned(),
147+
)
148+
.title(Line::styled(active_tag, Style::default().add_modifier(Modifier::ITALIC)).right_aligned()),
127149
area,
128150
);
129151
Ok(())

‎src/panes/profiles.rs

+15-2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ impl ProfilesPane {
2828
false => Style::default(),
2929
}
3030
}
31+
32+
fn border_type(&self) -> BorderType {
33+
match self.focused {
34+
true => BorderType::Thick,
35+
false => BorderType::Plain,
36+
}
37+
}
3138
}
3239
impl Pane for ProfilesPane {
3340
fn init(&mut self) -> Result<()> {
@@ -58,8 +65,14 @@ impl Pane for ProfilesPane {
5865
}
5966

6067
fn draw(&mut self, frame: &mut Frame<'_>, area: Rect) -> Result<()> {
61-
frame
62-
.render_widget(Block::default().title("Profiles").borders(Borders::ALL).border_style(self.border_style()), area);
68+
frame.render_widget(
69+
Block::default()
70+
.title("Profiles")
71+
.borders(Borders::ALL)
72+
.border_style(self.border_style())
73+
.border_type(self.border_type()),
74+
area,
75+
);
6376
Ok(())
6477
}
6578
}

‎src/panes/request.rs

+15-2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ impl RequestPane {
2828
false => Style::default(),
2929
}
3030
}
31+
32+
fn border_type(&self) -> BorderType {
33+
match self.focused {
34+
true => BorderType::Thick,
35+
false => BorderType::Plain,
36+
}
37+
}
3138
}
3239
impl Pane for RequestPane {
3340
fn init(&mut self) -> Result<()> {
@@ -58,8 +65,14 @@ impl Pane for RequestPane {
5865
}
5966

6067
fn draw(&mut self, frame: &mut Frame<'_>, area: Rect) -> Result<()> {
61-
frame
62-
.render_widget(Block::default().title("Request").borders(Borders::ALL).border_style(self.border_style()), area);
68+
frame.render_widget(
69+
Block::default()
70+
.title("Request")
71+
.borders(Borders::ALL)
72+
.border_style(self.border_style())
73+
.border_type(self.border_type()),
74+
area,
75+
);
6376
Ok(())
6477
}
6578
}

‎src/panes/response.rs

+15-2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ impl ResponsePane {
2828
false => Style::default(),
2929
}
3030
}
31+
32+
fn border_type(&self) -> BorderType {
33+
match self.focused {
34+
true => BorderType::Thick,
35+
false => BorderType::Plain,
36+
}
37+
}
3138
}
3239
impl Pane for ResponsePane {
3340
fn init(&mut self) -> Result<()> {
@@ -58,8 +65,14 @@ impl Pane for ResponsePane {
5865
}
5966

6067
fn draw(&mut self, frame: &mut Frame<'_>, area: Rect) -> Result<()> {
61-
frame
62-
.render_widget(Block::default().title("Response").borders(Borders::ALL).border_style(self.border_style()), area);
68+
frame.render_widget(
69+
Block::default()
70+
.title("Response")
71+
.borders(Borders::ALL)
72+
.border_style(self.border_style())
73+
.border_type(self.border_type()),
74+
area,
75+
);
6376
Ok(())
6477
}
6578
}

‎src/panes/tags.rs

+75-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::sync::{Arc, RwLock};
2+
13
use color_eyre::eyre::Result;
24
use crossterm::event::{KeyEvent, MouseEvent};
35
use ratatui::{
@@ -7,6 +9,7 @@ use ratatui::{
79

810
use crate::{
911
action::Action,
12+
pages::home::State,
1013
panes::Pane,
1114
tui::{EventResponse, Frame},
1215
};
@@ -15,11 +18,13 @@ use crate::{
1518
pub struct TagsPane {
1619
focused: bool,
1720
focused_border_style: Style,
21+
state: Arc<RwLock<State>>,
22+
current_tag_index: usize,
1823
}
1924

2025
impl TagsPane {
21-
pub fn new(focused: bool, focused_border_style: Style) -> Self {
22-
Self { focused, focused_border_style }
26+
pub fn new(state: Arc<RwLock<State>>, focused: bool, focused_border_style: Style) -> Self {
27+
Self { focused, focused_border_style, state, current_tag_index: 0 }
2328
}
2429

2530
fn border_style(&self) -> Style {
@@ -28,6 +33,13 @@ impl TagsPane {
2833
false => Style::default(),
2934
}
3035
}
36+
37+
fn border_type(&self) -> BorderType {
38+
match self.focused {
39+
true => BorderType::Thick,
40+
false => BorderType::Plain,
41+
}
42+
}
3143
}
3244
impl Pane for TagsPane {
3345
fn init(&mut self) -> Result<()> {
@@ -53,12 +65,71 @@ impl Pane for TagsPane {
5365
Ok(None)
5466
}
5567

56-
fn update(&mut self, _action: Action) -> Result<Option<Action>> {
68+
fn update(&mut self, action: Action) -> Result<Option<Action>> {
69+
match action {
70+
Action::Down => {
71+
let state = self.state.read().unwrap();
72+
let tags_list_len = state.openapi_spec.tags.len().saturating_add(1);
73+
if tags_list_len > 0 {
74+
self.current_tag_index = self.current_tag_index.saturating_add(1) % tags_list_len;
75+
}
76+
},
77+
Action::Up => {
78+
let state = self.state.read().unwrap();
79+
let tags_list_len = state.openapi_spec.tags.len().saturating_add(1);
80+
if tags_list_len > 0 {
81+
self.current_tag_index = self.current_tag_index.saturating_add(tags_list_len - 1) % tags_list_len;
82+
}
83+
},
84+
Action::Submit => {
85+
let mut state = self.state.write().unwrap();
86+
if self.current_tag_index > 0 {
87+
if let Some(tag) = state.openapi_spec.tags.get(self.current_tag_index - 1) {
88+
state.active_tag_name = Some(tag.name.clone());
89+
state.active_operation_index = 0;
90+
}
91+
} else {
92+
state.active_tag_name = None;
93+
state.active_operation_index = 0;
94+
}
95+
return Ok(Some(Action::Update));
96+
},
97+
_ => {},
98+
}
99+
57100
Ok(None)
58101
}
59102

60103
fn draw(&mut self, frame: &mut Frame<'_>, area: Rect) -> Result<()> {
61-
frame.render_widget(Block::default().title("Tags").borders(Borders::ALL).border_style(self.border_style()), area);
104+
let state = self.state.read().unwrap();
105+
let mut items: Vec<Line<'_>> = state
106+
.openapi_spec
107+
.tags
108+
.iter()
109+
.map(|tag| Line::from(vec![Span::styled(tag.name.as_str(), Style::default())]))
110+
.collect();
111+
112+
items.insert(0, Line::styled("[ALL]", Style::default()));
113+
114+
let list = List::new(items)
115+
.block(Block::default().borders(Borders::ALL))
116+
.highlight_style(Style::default().add_modifier(Modifier::BOLD).bg(Color::DarkGray))
117+
.direction(ListDirection::TopToBottom);
118+
let mut list_state = ListState::default().with_selected(Some(self.current_tag_index));
119+
120+
frame.render_stateful_widget(list, area, &mut list_state);
121+
let items_len = state.openapi_spec.tags.len() + 1;
122+
frame.render_widget(
123+
Block::default()
124+
.title("Tags")
125+
.borders(Borders::ALL)
126+
.border_style(self.border_style())
127+
.border_type(self.border_type())
128+
.title_bottom(
129+
Line::from(format!("{} of {}", self.current_tag_index.saturating_add(1), items_len)).right_aligned(),
130+
),
131+
area,
132+
);
62133
Ok(())
63134
}
64135
}

‎static/demo.gif

88.1 KB
Loading

‎static/demo.tape

+8-1
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,19 @@ Type "cargo run -q -- --openapi-path examples/petstore.json"
1616
Sleep 2s
1717
Enter
1818
Sleep 1s
19-
Down@500ms 4
19+
Down@1s 4
2020
Enter
2121
Sleep 1s
2222
Up@500ms 2
2323
Enter
2424
Sleep 1s
25+
Right
26+
Down@1s 3
27+
Enter
28+
Sleep 1s
29+
Left
30+
Down@1s 4
31+
Enter
2532
Right@500ms 6
2633
Sleep 4s
2734
Type "q"

0 commit comments

Comments
 (0)
Please sign in to comment.