Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use formatted table for query parameters and headers #28

Merged
merged 2 commits into from
Nov 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
- Differentiate history between different collections [#10](https://github.com/LucasPickering/slumber/issues/10)
- Ensure ctrl-c can't get eaten by text boxes (it guarantees exit now) [#18](https://github.com/LucasPickering/slumber/issues/18)

### Changed

- Adjust size of profile list dynamically based on number of profiles
- Use structured table display format for query parameters and headers
- Tweak list and tab styling

## [0.4.0] - 2023-11-02

### Added
Expand Down
2 changes: 2 additions & 0 deletions slumber.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ requests:
- id: login
method: POST
url: "{{host}}/anything/login"
query:
sudo: yes_please
body: |
{
"username": "{{chains.username}}",
Expand Down
1 change: 1 addition & 0 deletions src/tui/view/component/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod primary;
mod request;
mod response;
mod root;
mod table;
mod tabs;
mod text_window;

Expand Down
9 changes: 8 additions & 1 deletion src/tui/view/component/primary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,14 @@ impl<'a> Draw<PrimaryViewProps<'a>> for PrimaryView {
let [profiles_chunk, recipes_chunk] = layout(
left_chunk,
Direction::Vertical,
[Constraint::Max(16), Constraint::Min(0)],
[
// Make profile list as small as possible, with a max size
Constraint::Max(
self.profile_list_pane.profiles.len().clamp(1, 16) as u16
+ 2, // Account for top/bottom border
),
Constraint::Min(0),
],
);

// Split right column vertically
Expand Down
38 changes: 25 additions & 13 deletions src/tui/view/component/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::{
component::{
primary::PrimaryPane,
root::FullscreenMode,
table::{Table, TableProps},
tabs::Tabs,
text_window::{TextWindow, TextWindowProps},
Component, Draw, Event, Update, UpdateContext,
Expand Down Expand Up @@ -110,22 +111,33 @@ impl<'a> Draw<RequestPaneProps<'a>> for RequestPane {
// Request content
match self.tabs.selected() {
Tab::Body => {
let text = recipe.body.as_deref().unwrap_or_default();
self.text_window.draw(
context,
TextWindowProps {
key: &recipe.id,
text,
},
content_chunk,
);
if let Some(text) = recipe.body.as_deref() {
self.text_window.draw(
context,
TextWindowProps {
key: &recipe.id,
text,
},
content_chunk,
);
}
}
Tab::Query => context.frame.render_widget(
Paragraph::new(recipe.query.to_tui(context)),
Tab::Query => Table.draw(
context,
TableProps {
key_label: "Parameter",
value_label: "Value",
data: &recipe.query,
},
content_chunk,
),
Tab::Headers => context.frame.render_widget(
Paragraph::new(recipe.headers.to_tui(context)),
Tab::Headers => Table.draw(
context,
TableProps {
key_label: "Header",
value_label: "Value",
data: &recipe.headers,
},
content_chunk,
),
}
Expand Down
14 changes: 11 additions & 3 deletions src/tui/view/component/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ use crate::{
component::{
primary::PrimaryPane,
root::FullscreenMode,
table::{Table, TableProps},
tabs::Tabs,
text_window::{TextWindow, TextWindowProps},
Component, Draw, Event, Update, UpdateContext,
},
state::{FixedSelect, RequestState},
util::{layout, BlockBrick, ToTui},
util::{layout, BlockBrick, HeaderValueDisplay, ToTui},
DrawContext,
},
},
Expand Down Expand Up @@ -175,8 +176,15 @@ impl<'a> Draw<ResponsePaneProps<'a>> for ResponsePane {
},
content_chunk,
),
Tab::Headers => context.frame.render_widget(
Paragraph::new(response.headers.to_tui(context)),
Tab::Headers => Table.draw(
context,
TableProps {
key_label: "Header",
value_label: "Value",
data: response.headers.iter().map(|(k, v)| {
(k, HeaderValueDisplay::from(v))
}),
},
content_chunk,
),
}
Expand Down
51 changes: 51 additions & 0 deletions src/tui/view/component/table.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use crate::tui::view::component::{Draw, DrawContext};
use derive_more::Display;
use ratatui::{
prelude::{Constraint, Rect},
widgets::Row,
};
use std::fmt::Display;

/// 2-column tabular data display
#[derive(Debug, Display)]
pub struct Table;

pub struct TableProps<'a, T> {
pub key_label: &'a str,
pub value_label: &'a str,
pub data: T,
}

/// Any (key, value) iterator can be drawn as a table, as long as the key and
/// value implement `Display`
impl<'a, K, V, Iter, Data> Draw<TableProps<'a, Data>> for Table
where
K: Display,
V: Display,
Iter: Iterator<Item = (K, V)>,
Data: IntoIterator<Item = (K, V), IntoIter = Iter>,
{
fn draw(
&self,
context: &mut DrawContext,
props: TableProps<'a, Data>,
chunk: Rect,
) {
let rows = props.data.into_iter().enumerate().map(|(i, (k, v))| {
// Alternate row style for readability
let style = if i % 2 == 0 {
context.theme.table_text_style
} else {
context.theme.table_alt_text_style
};
Row::new(vec![k.to_string(), v.to_string()]).style(style)
});
let table = ratatui::widgets::Table::new(rows)
.header(
Row::new(vec![props.key_label, props.value_label])
.style(context.theme.table_header_style),
)
.widths(&[Constraint::Percentage(50), Constraint::Percentage(50)]);
context.frame.render_widget(table, chunk)
}
}
2 changes: 1 addition & 1 deletion src/tui/view/component/tabs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ impl<T: FixedSelect> Draw for Tabs<T> {
T::iter().map(|e| e.to_string()).collect(),
)
.select(self.tabs.selected_index())
.highlight_style(context.theme.text_highlight_style),
.highlight_style(context.theme.tab_highlight_style),
chunk,
)
}
Expand Down
5 changes: 5 additions & 0 deletions src/tui/view/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@ impl<T> StatefulList<T> {
self.items.get(self.state.borrow().selected()?)
}

/// Get the number of items in the list
pub fn len(&self) -> usize {
self.items.len()
}

/// Get a mutable reference to state. This uses `RefCell` underneath so it
/// will panic if aliased. Only call this during the draw phase!
pub fn state_mut(&self) -> impl DerefMut<Target = ListState> + '_ {
Expand Down
56 changes: 42 additions & 14 deletions src/tui/view/theme.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,36 @@
use ratatui::style::{Color, Modifier, Style};

// Ideally this should be part of the theme, but that requires some sort of
// two-stage themeing
pub const PRIMARY_COLOR: Color = Color::LightGreen;

/// Configurable visual settings for the UI
#[derive(Debug)]
pub struct Theme {
pub pane_border_style: Style,
pub pane_border_focus_style: Style,
pub text_highlight_style: Style,
/// Style for line numbers on large text areas
/// Line numbers on large text areas
pub line_number_style: Style,
pub list_highlight_symbol: &'static str,

/// Highlighted item in a list
pub list_highlight_style: Style,

/// Pane border when not selected/focused
pub pane_border_style: Style,
/// Pane border when selected/focused
pub pane_border_selected_style: Style,

/// Highlighted tab in a tab group
pub tab_highlight_style: Style,

/// Table column header text
pub table_header_style: Style,
pub table_text_style: Style,
pub table_alt_text_style: Style,
}

impl Theme {
pub fn pane_border_style(&self, is_focused: bool) -> Style {
if is_focused {
self.pane_border_focus_style
self.pane_border_selected_style
} else {
self.pane_border_style
}
Expand All @@ -24,16 +40,28 @@ impl Theme {
impl Default for Theme {
fn default() -> Self {
Self {
pane_border_style: Style::default(),
pane_border_focus_style: Style::default()
.fg(Color::LightGreen)
.add_modifier(Modifier::BOLD),
text_highlight_style: Style::default()
.bg(Color::LightGreen)
line_number_style: Style::default(),

list_highlight_style: Style::default()
.bg(PRIMARY_COLOR)
.fg(Color::Black)
.add_modifier(Modifier::BOLD),
line_number_style: Style::default(),
list_highlight_symbol: ">> ",

pane_border_style: Style::default(),
pane_border_selected_style: Style::default()
.fg(PRIMARY_COLOR)
.add_modifier(Modifier::BOLD),

tab_highlight_style: Style::default()
.fg(PRIMARY_COLOR)
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::UNDERLINED),

table_header_style: Style::default()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::UNDERLINED),
table_text_style: Style::default(),
table_alt_text_style: Style::default().bg(Color::DarkGray),
}
}
}
54 changes: 19 additions & 35 deletions src/tui/view/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ use crate::{
},
};
use chrono::{DateTime, Duration, Local, Utc};
use indexmap::IndexMap;
use derive_more::From;
use ratatui::{
prelude::*,
text::{Line, Span, Text},
widgets::{Block, Borders, List, ListItem},
};
use reqwest::header::HeaderMap;
use std::fmt::Display;
use reqwest::header::HeaderValue;
use std::fmt::{Display, Formatter};

/// A helper for building a UI. It can be converted into some UI element to be
/// drawn.
Expand Down Expand Up @@ -56,7 +56,7 @@ impl<'a> ToTui for ButtonBrick<'a> {
type Output<'this> = Text<'this> where Self: 'this;

fn to_tui(&self, context: &DrawContext) -> Self::Output<'_> {
Text::styled(self.text, context.theme.text_highlight_style)
Text::styled(self.text, context.theme.list_highlight_style)
}
}

Expand All @@ -82,8 +82,7 @@ impl<'a, T: ToTui<Output<'a> = Span<'a>>> ToTui for ListBrick<'a, T> {

List::new(items)
.block(block)
.highlight_style(context.theme.text_highlight_style)
.highlight_symbol(context.theme.list_highlight_symbol)
.highlight_style(context.theme.list_highlight_style)
}
}

Expand Down Expand Up @@ -154,35 +153,6 @@ impl ToTui for Option<Duration> {
}
}

impl<K: Display, V: Display> ToTui for IndexMap<K, V> {
type Output<'this> = Text<'this> where Self: 'this;

fn to_tui(&self, _context: &DrawContext) -> Self::Output<'_> {
self.iter()
.map(|(key, value)| format!("{key} = {value}").into())
.collect::<Vec<Line>>()
.into()
}
}

impl ToTui for HeaderMap {
/// 'static because string is generated
type Output<'this> = Text<'static>;

fn to_tui(&self, _context: &DrawContext) -> Self::Output<'_> {
self.iter()
.map(|(key, value)| {
format!(
"{key} = {}",
value.to_str().unwrap_or("<unrepresentable>")
)
.into()
})
.collect::<Vec<Line>>()
.into()
}
}

impl ToTui for anyhow::Error {
/// 'static because string is generated
type Output<'this> = Text<'static>;
Expand Down Expand Up @@ -216,6 +186,20 @@ impl ToTui for RequestError {
}
}

/// Wrapper to implement `Display` for an HTTP header value. Uses a placeholder
/// value for non-UTF-8 values
#[derive(Debug, From)]
pub struct HeaderValueDisplay<'a>(&'a HeaderValue);

impl<'a> Display for HeaderValueDisplay<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self.0.to_str() {
Ok(s) => write!(f, "{}", s),
Err(_) => write!(f, "<invalid utf-8>"),
}
}
}

/// Helper for building a layout with a fixed number of constraints
pub fn layout<const N: usize>(
area: Rect,
Expand Down
Loading