Skip to content

Commit

Permalink
Merge pull request #13 from zaghaghi/12-add-support-for-searching-in-…
Browse files Browse the repository at this point in the history
…the-apis-pane

12 add support for searching in the apis pane
  • Loading branch information
zaghaghi authored Mar 15, 2024
2 parents dd7d6d6 + 9a18cca commit 786f47f
Show file tree
Hide file tree
Showing 11 changed files with 197 additions and 32 deletions.
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ tokio-util = "0.7.9"
tracing = "0.1.37"
tracing-error = "0.2.0"
tracing-subscriber = { version = "0.3.17", features = ["env-filter", "serde"] }
tui-input = "0.8.0"

[build-dependencies]
vergen = { version = "8.2.6", features = ["build", "git", "gitoxide", "cargo"] }
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ Terminal UI to list, browse and run APIs defined with OpenAPI v3.0 spec.
## Webhooks
![webhooks](static/webhooks.gif)

## Filter
![filter](static/filter.gif)

# Installation
Install from source:
```bash
Expand Down Expand Up @@ -66,14 +69,16 @@ Options:
| `1...9` | Move between tabs |
| `f` | Toggle fullscreen pane|
| `g` | Go in nested items in lists|
| `/` | Filter apis|
| `Backspace`, `b` | Get out of nested items in lists|


# Milestones
# Features
- [X] Viewer
- [X] OpenAPI v3.1
- [X] Display Webhooks
- [X] Display Info and Version
- [X] Display Info and Version
- [X] Search #12
- [ ] Display Key Mappings in Popup
- [ ] Execute
- [ ] Remote API specification
Expand Down
3 changes: 3 additions & 0 deletions src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@ pub enum Action {
Go,
Back,
ToggleFullScreen,
FocusFooter,
Filter(String),
Noop,
}
89 changes: 71 additions & 18 deletions src/pages/home.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ use crate::{
tui::EventResponse,
};

#[derive(Default)]
pub enum InputMode {
#[default]
Normal,
Insert,
}

pub enum OperationItemType {
Path,
Webhook,
Expand All @@ -44,6 +51,7 @@ pub struct State {
pub openapi_operations: Vec<OperationItem>,
pub active_operation_index: usize,
pub active_tag_name: Option<String>,
pub active_filter: String,
}

impl State {
Expand All @@ -52,18 +60,32 @@ impl State {
self
.openapi_operations
.iter()
.filter(|flat_operation| flat_operation.has_tag(active_tag))
.filter(|flat_operation| {
flat_operation.has_tag(active_tag) && flat_operation.path.contains(self.active_filter.as_str())
})
.nth(self.active_operation_index)
} else {
self.openapi_operations.get(self.active_operation_index)
self
.openapi_operations
.iter()
.filter(|flat_operation| flat_operation.path.contains(self.active_filter.as_str()))
.nth(self.active_operation_index)
}
}

pub fn operations_len(&self) -> usize {
if let Some(active_tag) = &self.active_tag_name {
self.openapi_operations.iter().filter(|item| item.has_tag(active_tag)).count()
self
.openapi_operations
.iter()
.filter(|item| item.has_tag(active_tag) && item.path.contains(self.active_filter.as_str()))
.count()
} else {
self.openapi_operations.len()
self
.openapi_operations
.iter()
.filter(|flat_operation| flat_operation.path.contains(self.active_filter.as_str()))
.count()
}
}
}
Expand All @@ -78,6 +100,7 @@ pub struct Home {
#[allow(dead_code)]
state: Arc<RwLock<State>>,
fullscreen_pane_index: Option<usize>,
input_mode: InputMode,
}

impl Home {
Expand All @@ -99,6 +122,7 @@ impl Home {
openapi_operations,
active_operation_index: 0,
active_tag_name: None,
active_filter: String::default(),
}));
let focused_border_style = Style::default().fg(Color::LightGreen);

Expand All @@ -116,6 +140,7 @@ impl Home {
focused_pane_index: 0,
state,
fullscreen_pane_index: None,
input_mode: InputMode::Normal,
})
}
}
Expand Down Expand Up @@ -169,6 +194,26 @@ impl Page for Home {
Action::ToggleFullScreen => {
self.fullscreen_pane_index = self.fullscreen_pane_index.map_or(Some(self.focused_pane_index), |_| None);
},
Action::FocusFooter => {
if let Some(pane) = self.panes.get_mut(self.focused_pane_index) {
pane.unfocus()?;
}
self.static_panes[1].focus()?;
self.input_mode = InputMode::Insert;
},
Action::Filter(filter) => {
self.static_panes[1].unfocus()?;
if let Some(pane) = self.panes.get_mut(self.focused_pane_index) {
pane.focus()?;
}
self.input_mode = InputMode::Normal;
{
let mut state = self.state.write().unwrap();
state.active_operation_index = 0;
state.active_filter = filter;
}
return Ok(Some(Action::Update));
},
_ => {
if let Some(pane) = self.panes.get_mut(self.focused_pane_index) {
return pane.update(action);
Expand All @@ -179,21 +224,29 @@ impl Page for Home {
}

fn handle_key_events(&mut self, key: KeyEvent) -> Result<Option<EventResponse<Action>>> {
let response = match key.code {
KeyCode::Right | KeyCode::Char('l') | KeyCode::Char('L') => EventResponse::Stop(Action::FocusNext),
KeyCode::Left | KeyCode::Char('h') | KeyCode::Char('H') => EventResponse::Stop(Action::FocusPrev),
KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => EventResponse::Stop(Action::Down),
KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => EventResponse::Stop(Action::Up),
KeyCode::Char('g') | KeyCode::Char('G') => EventResponse::Stop(Action::Go),
KeyCode::Backspace | KeyCode::Char('b') | KeyCode::Char('B') => EventResponse::Stop(Action::Back),
KeyCode::Enter => EventResponse::Stop(Action::Submit),
KeyCode::Char('f') | KeyCode::Char('F') => EventResponse::Stop(Action::ToggleFullScreen),
KeyCode::Char(c) if ('1'..='9').contains(&c) => EventResponse::Stop(Action::Tab(c.to_digit(10).unwrap_or(0) - 1)),
_ => {
return Ok(None);
match self.input_mode {
InputMode::Normal => {
let response = match key.code {
KeyCode::Right | KeyCode::Char('l') | KeyCode::Char('L') => EventResponse::Stop(Action::FocusNext),
KeyCode::Left | KeyCode::Char('h') | KeyCode::Char('H') => EventResponse::Stop(Action::FocusPrev),
KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => EventResponse::Stop(Action::Down),
KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => EventResponse::Stop(Action::Up),
KeyCode::Char('g') | KeyCode::Char('G') => EventResponse::Stop(Action::Go),
KeyCode::Backspace | KeyCode::Char('b') | KeyCode::Char('B') => EventResponse::Stop(Action::Back),
KeyCode::Enter => EventResponse::Stop(Action::Submit),
KeyCode::Char('f') | KeyCode::Char('F') => EventResponse::Stop(Action::ToggleFullScreen),
KeyCode::Char(c) if ('1'..='9').contains(&c) => {
EventResponse::Stop(Action::Tab(c.to_digit(10).unwrap_or(0) - 1))
},
KeyCode::Char('/') => EventResponse::Stop(Action::FocusFooter),
_ => {
return Ok(None);
},
};
Ok(Some(response))
},
};
Ok(Some(response))
InputMode::Insert => self.static_panes[1].handle_key_events(key),
}
}

fn draw(&mut self, frame: &mut Frame<'_>, area: Rect) -> Result<()> {
Expand Down
4 changes: 2 additions & 2 deletions src/panes/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,9 @@ impl Pane for AddressPane {
let state = self.state.read().unwrap();
if let Some(operation_item) = state.active_operation() {
let base_url = if let Some(server) = state.openapi_spec.servers.as_ref().map(|v| v.first()).unwrap_or(None) {
server.url.clone()
String::from(server.url.trim_end_matches('/'))
} else if let Some(server) = operation_item.operation.servers.as_ref().map(|v| v.first()).unwrap_or(None) {
server.url.clone()
String::from(server.url.trim_end_matches('/'))
} else {
String::from("http://localhost")
};
Expand Down
3 changes: 3 additions & 0 deletions src/panes/apis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ impl Pane for ApisPane {
return None;
}
}
if !operation_item.path.contains(state.active_filter.as_str()) {
return None;
}
Some(Line::from(vec![
Span::styled(
format!(" {:7}", match operation_item.r#type {
Expand Down
73 changes: 63 additions & 10 deletions src/panes/footer.rs
Original file line number Diff line number Diff line change
@@ -1,36 +1,89 @@
use std::sync::{Arc, RwLock};

use color_eyre::eyre::Result;
use ratatui::prelude::*;
use crossterm::event::{Event, KeyCode, KeyEvent};
use ratatui::{prelude::*, widgets::Paragraph};
use tui_input::{backend::crossterm::EventHandler, Input};

use crate::{pages::home::State, panes::Pane, tui::Frame};
use crate::{
action::Action,
pages::home::State,
panes::Pane,
tui::{EventResponse, Frame},
};

#[derive(Default)]
pub struct FooterPane {
focused: bool,
#[allow(dead_code)]
state: Arc<RwLock<State>>,
input: Input,
}

impl FooterPane {
pub fn new(state: Arc<RwLock<State>>) -> Self {
Self { state }
Self { focused: false, state, input: Input::default() }
}
}

impl Pane for FooterPane {
fn focus(&mut self) -> Result<()> {
self.focused = true;
Ok(())
}

fn unfocus(&mut self) -> Result<()> {
self.focused = false;
Ok(())
}

fn height_constraint(&self) -> Constraint {
Constraint::Max(1)
}

fn handle_key_events(&mut self, key: KeyEvent) -> Result<Option<EventResponse<Action>>> {
self.input.handle_event(&Event::Key(key));
let response = match key.code {
KeyCode::Enter => Some(EventResponse::Stop(Action::Filter(self.input.to_string()))),
KeyCode::Esc => {
let filter: String;
{
let state = self.state.read().unwrap();
filter = state.active_filter.clone();
}
Some(EventResponse::Stop(Action::Filter(filter)))
},
_ => Some(EventResponse::Stop(Action::Noop)),
};
Ok(response)
}

fn draw(&mut self, frame: &mut Frame<'_>, area: Rect) -> Result<()> {
const ARROW: &str = symbols::scrollbar::HORIZONTAL.end;
frame.render_widget(
Line::from(vec![
Span::styled(format!("[l/h {ARROW} next/prev pane] [j/k {ARROW} next/prev item] [1-9 {ARROW} select tab] [g/b {ARROW} go/back definitions] [q {ARROW} quit]"), Style::default()),
])
.style(Style::default().fg(Color::DarkGray)),
area,
);
if self.focused {
let search_label = "Filter: ";
let width = area.width.max(3);
let scroll = self.input.visual_scroll(width as usize);
let input = Paragraph::new(Line::from(vec![
Span::styled(search_label, Style::default().fg(Color::LightBlue)),
Span::styled(self.input.value(), Style::default()),
]))
.scroll((0, scroll as u16));
frame.render_widget(input, area);

frame.set_cursor(
area.x + ((self.input.visual_cursor()).max(scroll) - scroll) as u16 + search_label.len() as u16,
area.y + 1,
)
} else {
frame.render_widget(
Line::from(vec![
Span::styled(format!("[l,h,j,k {ARROW} movement] [/ {ARROW} filter] [1-9 {ARROW} select tab] [g,b {ARROW} go/back definitions] [q {ARROW} quit]"), Style::default()),
])
.style(Style::default().fg(Color::DarkGray)),
area,
);
}
Ok(())
}
}
2 changes: 2 additions & 0 deletions src/panes/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ impl ResponsePane {
fn init_schema(&mut self) -> Result<()> {
{
let state = self.state.read().unwrap();
self.schemas = vec![];

if let Some(operation_item) = state.active_operation() {
self.schemas = operation_item
.operation
Expand Down
Binary file added static/filter.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions static/filter.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
Output static/filter.gif
Hide
Set Shell zsh

Set FontSize 16
Set Width 1200
Set Height 600

Set Padding 20
Set FontFamily "JetBrains Mono"

Set WindowBar Colorful
Set WindowBarSize 40

Set Theme "Catppuccin Mocha"
Type "cargo run -- --openapi-path examples/stripe/spec.yml"
Enter
Sleep 10s
Set TypingSpeed 400ms
Show
Sleep 2s
Type "/"
Sleep 2s
Type "balance"
Enter
Sleep 2s
Type "j"
Type "j"

Sleep 5s
Hide
Type "q"
Ctrl+D
Show

0 comments on commit 786f47f

Please sign in to comment.