use std::path::PathBuf;
use ratatui::Frame;
use ratatui::layout::{Constraint, Layout, Position, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{
Block, Borders, Clear, List, ListItem, ListState, Paragraph, Scrollbar, ScrollbarOrientation,
ScrollbarState,
};
use crate::hurl::{FormFieldKind, METHODS};
use crate::i18n::Strings;
use super::draw::*;
use super::editor::*;
use super::theme::*;
pub(crate) const COMMON_HEADERS: &[&str] = &[
"Accept",
"Accept-Charset",
"Accept-Encoding",
"Accept-Language",
"Authorization",
"Cache-Control",
"Connection",
"Content-Length",
"Content-Type",
"Cookie",
"Date",
"ETag",
"Expect",
"Host",
"If-Match",
"If-Modified-Since",
"If-None-Match",
"Origin",
"Pragma",
"Range",
"Referer",
"User-Agent",
"X-Api-Key",
"X-Content-Type-Options",
"X-Correlation-ID",
"X-CSRF-Token",
"X-Forwarded-For",
"X-Forwarded-Host",
"X-Forwarded-Proto",
"X-Frame-Options",
"X-Request-ID",
"X-Requested-With",
];
pub(crate) const CONTENT_TYPE_TABLE: &[(&str, &str)] = &[
("gif", "image/gif"),
("jpg", "image/jpeg"),
("jpeg", "image/jpeg"),
("png", "image/png"),
("svg", "image/svg+xml"),
("txt", "text/plain"),
("htm", "text/html"),
("html", "text/html"),
("pdf", "application/pdf"),
("xml", "application/xml"),
("webm", "video/webm"),
("mp4", "video/mp4"),
("m4v", "video/mp4"),
("mov", "video/quicktime"),
("avi", "video/x-msvideo"),
("wmv", "video/x-ms-wmv"),
("mkv", "video/x-matroska"),
("mpeg", "video/mpeg"),
("mpg", "video/mpeg"),
("3gp", "video/3gpp"),
("ogv", "video/ogg"),
];
pub(crate) fn infer_content_type(path: &str) -> Option<&'static str> {
let ext = std::path::Path::new(path)
.extension()?
.to_str()?
.to_ascii_lowercase();
CONTENT_TYPE_TABLE
.iter()
.find(|(e, _)| *e == ext)
.map(|(_, m)| *m)
}
pub(crate) const COMMON_CONTENT_TYPES: &[&str] = &[
"application/json",
"application/x-www-form-urlencoded",
"multipart/form-data",
"application/octet-stream",
"text/css",
"application/javascript",
"text/csv",
"application/x-yaml",
];
pub(crate) fn content_type_options() -> Vec<&'static str> {
let mut out: Vec<&'static str> = Vec::new();
for (_, mime) in CONTENT_TYPE_TABLE {
if !out.contains(mime) {
out.push(mime);
}
}
for mime in COMMON_CONTENT_TYPES {
if !out.contains(mime) {
out.push(mime);
}
}
out.sort_unstable();
out
}
pub(crate) fn filter_content_types(query: &str) -> Vec<&'static str> {
let q = query.trim().to_ascii_lowercase();
if q.is_empty() {
return content_type_options();
}
content_type_options()
.into_iter()
.filter(|o| o.to_ascii_lowercase().contains(&q))
.collect()
}
pub(crate) fn filter_headers(query: &str) -> Vec<&'static str> {
let q = query.trim().to_ascii_lowercase();
if q.is_empty() {
return COMMON_HEADERS.to_vec();
}
COMMON_HEADERS
.iter()
.copied()
.filter(|h| h.to_ascii_lowercase().contains(&q))
.collect()
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub(crate) enum HdrCol {
Key,
Value,
Desc,
Enabled,
}
pub(crate) struct HeaderRow {
pub(crate) key: Editor,
pub(crate) value: Editor,
pub(crate) desc: Editor,
pub(crate) enabled: bool,
}
impl HeaderRow {
pub(crate) fn new() -> Self {
Self {
key: Editor::new("", false),
value: Editor::new("", false),
desc: Editor::new("", false),
enabled: true,
}
}
pub(crate) fn is_blank(&self) -> bool {
self.key.text().is_empty() && self.value.text().is_empty() && self.desc.text().is_empty()
}
pub(crate) fn cell_mut(&mut self, col: HdrCol) -> Option<&mut Editor> {
match col {
HdrCol::Key => Some(&mut self.key),
HdrCol::Value => Some(&mut self.value),
HdrCol::Desc => Some(&mut self.desc),
HdrCol::Enabled => None,
}
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub(crate) enum FormCol {
Key,
Value,
Kind,
Ctype,
Desc,
Enabled,
}
pub(crate) struct FormRow {
pub(crate) key: Editor,
pub(crate) value: Editor,
pub(crate) ctype: Editor,
pub(crate) desc: Editor,
pub(crate) enabled: bool,
pub(crate) kind: FormFieldKind,
}
impl FormRow {
pub(crate) fn new() -> Self {
Self {
key: Editor::new("", false),
value: Editor::new("", false),
ctype: Editor::new("", false),
desc: Editor::new("", false),
enabled: true,
kind: FormFieldKind::Text,
}
}
pub(crate) fn is_blank(&self) -> bool {
self.key.text().is_empty()
&& self.value.text().is_empty()
&& self.ctype.text().is_empty()
&& self.desc.text().is_empty()
}
pub(crate) fn cell_mut(&mut self, col: FormCol) -> Option<&mut Editor> {
match col {
FormCol::Key => Some(&mut self.key),
FormCol::Value => Some(&mut self.value),
FormCol::Ctype => Some(&mut self.ctype),
FormCol::Desc => Some(&mut self.desc),
FormCol::Kind | FormCol::Enabled => None,
}
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub(crate) enum CapCol {
Name,
Expr,
}
pub(crate) struct CaptureRow {
pub(crate) name: Editor,
pub(crate) expr: Editor,
}
impl CaptureRow {
pub(crate) fn new() -> Self {
Self {
name: Editor::new("", false),
expr: Editor::new("", false),
}
}
pub(crate) fn is_blank(&self) -> bool {
self.name.text().is_empty() && self.expr.text().is_empty()
}
pub(crate) fn cell_mut(&mut self, col: CapCol) -> &mut Editor {
match col {
CapCol::Name => &mut self.name,
CapCol::Expr => &mut self.expr,
}
}
}
pub(crate) struct AssertRow {
pub(crate) expr: Editor,
}
impl AssertRow {
pub(crate) fn new() -> Self {
Self {
expr: Editor::new("", false),
}
}
pub(crate) fn is_blank(&self) -> bool {
self.expr.text().is_empty()
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub(crate) enum NewField {
Name,
Target,
Method,
Url,
Header(usize, HdrCol),
AddHeader,
Cookie(usize, HdrCol),
AddCookie,
FormField(usize, FormCol),
AddFormField,
Body,
Assert(usize),
AddAssert,
Capture(usize, CapCol),
AddCapture,
}
impl NewField {
pub(crate) fn wizard_section(self) -> Option<WizardTab> {
match self {
NewField::Name | NewField::Target | NewField::Method | NewField::Url => None,
NewField::Header(..) | NewField::AddHeader => Some(WizardTab::Headers),
NewField::Cookie(..) | NewField::AddCookie => Some(WizardTab::Cookies),
NewField::FormField(..) | NewField::AddFormField => Some(WizardTab::Form),
NewField::Body => Some(WizardTab::Body),
NewField::Assert(..) | NewField::AddAssert => Some(WizardTab::Asserts),
NewField::Capture(..) | NewField::AddCapture => Some(WizardTab::Captures),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum WizardTab {
All,
Headers,
Cookies,
Form,
Body,
Asserts,
Captures,
}
impl WizardTab {
pub(crate) const ALL: [WizardTab; 7] = [
WizardTab::All,
WizardTab::Headers,
WizardTab::Cookies,
WizardTab::Form,
WizardTab::Body,
WizardTab::Asserts,
WizardTab::Captures,
];
pub(crate) fn label(self, s: &Strings) -> &'static str {
match self {
WizardTab::All => s.tab_all,
WizardTab::Headers => s.field_headers,
WizardTab::Cookies => s.field_cookies,
WizardTab::Form => s.field_form,
WizardTab::Body => s.field_body,
WizardTab::Asserts => s.field_asserts,
WizardTab::Captures => s.field_captures,
}
}
pub(crate) fn first_field(self) -> NewField {
match self {
WizardTab::All => NewField::Name,
WizardTab::Headers => NewField::Header(0, HdrCol::Key),
WizardTab::Cookies => NewField::Cookie(0, HdrCol::Key),
WizardTab::Form => NewField::FormField(0, FormCol::Key),
WizardTab::Body => NewField::Body,
WizardTab::Asserts => NewField::Assert(0),
WizardTab::Captures => NewField::Capture(0, CapCol::Name),
}
}
pub(crate) fn last_field(self) -> NewField {
match self {
WizardTab::All => NewField::Name,
WizardTab::Headers => NewField::AddHeader,
WizardTab::Cookies => NewField::AddCookie,
WizardTab::Form => NewField::AddFormField,
WizardTab::Body => NewField::Body,
WizardTab::Asserts => NewField::AddAssert,
WizardTab::Captures => NewField::AddCapture,
}
}
}
pub(crate) struct NewReq {
pub(crate) name: Editor,
pub(crate) url: Editor,
pub(crate) headers: Vec<HeaderRow>,
pub(crate) cookies: Vec<HeaderRow>,
pub(crate) form_fields: Vec<FormRow>,
pub(crate) body: Editor,
pub(crate) asserts: Vec<AssertRow>,
pub(crate) captures: Vec<CaptureRow>,
pub(crate) method_idx: usize,
pub(crate) focus: NewField,
pub(crate) target_idx: usize,
pub(crate) target_names: Vec<String>,
pub(crate) base_url: String,
pub(crate) suggest_hi: Option<usize>,
pub(crate) suggest_hidden: bool,
pub(crate) kind_dropdown_hidden: bool,
pub(crate) ctype_dropdown_hidden: bool,
pub(crate) ctype_hi: Option<usize>,
pub(crate) desc_visible: std::cell::Cell<bool>,
pub(crate) key_cell_rect: std::cell::Cell<Option<Rect>>,
pub(crate) kind_cell_rect: std::cell::Cell<Option<Rect>>,
pub(crate) ctype_cell_rect: std::cell::Cell<Option<Rect>>,
pub(crate) cookie_desc_visible: std::cell::Cell<bool>,
pub(crate) form_desc_visible: std::cell::Cell<bool>,
pub(crate) form_ctype_visible: std::cell::Cell<bool>,
pub(crate) header_scroll: std::cell::Cell<usize>,
pub(crate) cookie_scroll: std::cell::Cell<usize>,
pub(crate) form_scroll: std::cell::Cell<usize>,
pub(crate) assert_scroll: std::cell::Cell<usize>,
pub(crate) capture_scroll: std::cell::Cell<usize>,
pub(crate) file_root: Option<PathBuf>,
pub(crate) editing: Option<(usize, usize)>,
pub(crate) view_tab: WizardTab,
pub(crate) tab_order: Vec<WizardTab>,
}
impl NewReq {
pub(crate) fn new(
base_url: String,
target_names: Vec<String>,
target_idx: usize,
file_root: Option<PathBuf>,
) -> Self {
Self {
name: Editor::new("", false),
url: Editor::new("", false),
headers: Vec::new(),
cookies: Vec::new(),
form_fields: Vec::new(),
body: Editor::new("", true),
asserts: Vec::new(),
captures: Vec::new(),
method_idx: 0,
focus: NewField::Name,
target_idx: target_idx.min(target_names.len().saturating_sub(1)),
target_names,
base_url,
suggest_hi: None,
suggest_hidden: false,
kind_dropdown_hidden: false,
ctype_dropdown_hidden: false,
ctype_hi: None,
desc_visible: std::cell::Cell::new(true),
key_cell_rect: std::cell::Cell::new(None),
kind_cell_rect: std::cell::Cell::new(None),
ctype_cell_rect: std::cell::Cell::new(None),
cookie_desc_visible: std::cell::Cell::new(true),
form_desc_visible: std::cell::Cell::new(true),
form_ctype_visible: std::cell::Cell::new(true),
header_scroll: std::cell::Cell::new(0),
cookie_scroll: std::cell::Cell::new(0),
form_scroll: std::cell::Cell::new(0),
assert_scroll: std::cell::Cell::new(0),
capture_scroll: std::cell::Cell::new(0),
file_root,
editing: None,
view_tab: WizardTab::All,
tab_order: WizardTab::ALL.to_vec(),
}
}
pub(crate) fn from_entry(
ci: usize,
ei: usize,
entry: &crate::hurl::HurlEntry,
base_url: String,
target_names: Vec<String>,
file_root: Option<PathBuf>,
) -> Self {
let method_idx = METHODS.iter().position(|m| *m == entry.method).unwrap_or(0);
let headers = if entry.headers.is_empty() {
Vec::new()
} else {
entry
.headers
.iter()
.map(|(k, v)| {
let mut row = HeaderRow::new();
row.key = Editor::new(k, false);
row.value = Editor::new(v, false);
row
})
.collect()
};
let cookies = if entry.cookies.is_empty() {
Vec::new()
} else {
entry
.cookies
.iter()
.map(|(k, v)| {
let mut row = HeaderRow::new();
row.key = Editor::new(k, false);
row.value = Editor::new(v, false);
row
})
.collect()
};
let form_fields = if entry.form_fields.is_empty() {
Vec::new()
} else {
entry
.form_fields
.iter()
.map(|f| {
let mut row = FormRow::new();
row.key = Editor::new(&f.key, false);
row.value = Editor::new(&f.value, false);
row.ctype = Editor::new(f.content_type.as_deref().unwrap_or(""), false);
row.kind = f.kind;
row
})
.collect()
};
let asserts = entry
.asserts
.iter()
.map(|a| {
let mut row = AssertRow::new();
row.expr = Editor::new(a, false);
row
})
.collect();
let captures = entry
.captures
.iter()
.map(|(n, e)| {
let mut row = CaptureRow::new();
row.name = Editor::new(n, false);
row.expr = Editor::new(e, false);
row
})
.collect();
Self {
name: Editor::new(&entry.title, false),
url: Editor::new(&entry.url, false),
headers,
cookies,
form_fields,
body: Editor::new(entry.body.as_deref().unwrap_or(""), true),
asserts,
captures,
method_idx,
focus: NewField::Name,
target_idx: ci.min(target_names.len().saturating_sub(1)),
target_names,
base_url,
suggest_hi: None,
suggest_hidden: false,
kind_dropdown_hidden: false,
ctype_dropdown_hidden: false,
ctype_hi: None,
desc_visible: std::cell::Cell::new(true),
key_cell_rect: std::cell::Cell::new(None),
kind_cell_rect: std::cell::Cell::new(None),
ctype_cell_rect: std::cell::Cell::new(None),
cookie_desc_visible: std::cell::Cell::new(true),
form_desc_visible: std::cell::Cell::new(true),
form_ctype_visible: std::cell::Cell::new(true),
header_scroll: std::cell::Cell::new(0),
cookie_scroll: std::cell::Cell::new(0),
form_scroll: std::cell::Cell::new(0),
assert_scroll: std::cell::Cell::new(0),
capture_scroll: std::cell::Cell::new(0),
file_root,
editing: Some((ci, ei)),
view_tab: WizardTab::All,
tab_order: WizardTab::ALL.to_vec(),
}
}
pub(crate) fn target_name(&self) -> &str {
self.target_names
.get(self.target_idx)
.map(String::as_str)
.unwrap_or("Request")
}
pub(crate) fn method(&self) -> &'static str {
METHODS[self.method_idx.min(METHODS.len() - 1)]
}
fn key_suggestions(&self) -> Option<(usize, Vec<&'static str>)> {
let NewField::Header(i, HdrCol::Key) = self.focus else {
return None;
};
let text = self.headers.get(i)?.key.text();
let sugs = filter_headers(&text);
let single_exact = sugs.len() == 1 && sugs[0].eq_ignore_ascii_case(text.trim());
(!sugs.is_empty() && !single_exact).then_some((i, sugs))
}
pub(crate) fn key_dropdown(&self) -> Option<(usize, Vec<&'static str>)> {
if self.suggest_hidden {
return None;
}
self.key_suggestions()
}
pub(crate) fn key_dropdown_revealable(&self) -> bool {
self.suggest_hidden && self.key_suggestions().is_some()
}
pub(crate) fn accept_suggestion(&mut self, name: &str) {
if let NewField::Header(i, HdrCol::Key) = self.focus
&& let Some(row) = self.headers.get_mut(i)
{
row.key = Editor::new(name, false);
}
self.suggest_hi = None;
self.suggest_hidden = true;
}
pub(crate) fn kind_dropdown_open(&self) -> bool {
matches!(self.focus, NewField::FormField(_, FormCol::Kind)) && !self.kind_dropdown_hidden
}
pub(crate) fn kind_dropdown_revealable(&self) -> bool {
let NewField::FormField(_, FormCol::Kind) = self.focus else {
return false;
};
self.kind_dropdown_hidden
}
pub(crate) fn ctype_dropdown_open(&self) -> bool {
let NewField::FormField(i, FormCol::Ctype) = self.focus else {
return false;
};
!self.ctype_dropdown_hidden
&& self.form_fields.get(i).map(|r| r.kind) == Some(FormFieldKind::File)
}
pub(crate) fn ctype_dropdown_revealable(&self) -> bool {
let NewField::FormField(i, FormCol::Ctype) = self.focus else {
return false;
};
self.ctype_dropdown_hidden
&& self
.form_fields
.get(i)
.is_some_and(|r| r.kind == FormFieldKind::File && !r.ctype.text().is_empty())
}
pub(crate) fn ctype_filtered_options(&self) -> Vec<&'static str> {
let NewField::FormField(i, FormCol::Ctype) = self.focus else {
return Vec::new();
};
let query = self
.form_fields
.get(i)
.map(|r| r.ctype.text())
.unwrap_or_default();
filter_content_types(&query)
}
pub(crate) fn ctype_auto_visible(&self, s: &Strings) -> bool {
let NewField::FormField(i, FormCol::Ctype) = self.focus else {
return false;
};
let query = self
.form_fields
.get(i)
.map(|r| r.ctype.text())
.unwrap_or_default();
let q = query.trim();
q.is_empty()
|| s.content_type_auto
.to_ascii_lowercase()
.contains(&q.to_ascii_lowercase())
}
pub(crate) fn ctype_dropdown_entries(&self, s: &Strings) -> Vec<Option<&'static str>> {
let mut entries = Vec::new();
if self.ctype_auto_visible(s) {
entries.push(None);
}
entries.extend(self.ctype_filtered_options().into_iter().map(Some));
entries
}
pub(crate) fn ctype_option_count(&self, s: &Strings) -> usize {
self.ctype_dropdown_entries(s).len()
}
pub(crate) fn accept_content_type(&mut self, s: &Strings) {
let NewField::FormField(i, FormCol::Ctype) = self.focus else {
return;
};
let Some(hi) = self.ctype_hi else { return };
let entries = self.ctype_dropdown_entries(s);
let text = entries.get(hi).copied().flatten().unwrap_or("").to_string();
if let Some(row) = self.form_fields.get_mut(i) {
row.ctype = Editor::new(&text, false);
}
self.ctype_hi = None;
self.ctype_dropdown_hidden = true;
}
pub(crate) fn ctype_selected_index(&self, s: &Strings) -> Option<usize> {
if self.ctype_hi.is_some() {
return self.ctype_hi;
}
let NewField::FormField(i, FormCol::Ctype) = self.focus else {
return None;
};
let current = self.form_fields.get(i)?.ctype.text();
let entries = self.ctype_dropdown_entries(s);
if current.trim().is_empty() {
Some(0)
} else {
entries.iter().position(|e| *e == Some(current.trim()))
}
}
pub(crate) fn active_editor(&mut self) -> Option<&mut Editor> {
match self.focus {
NewField::Name => Some(&mut self.name),
NewField::Url => Some(&mut self.url),
NewField::Body => Some(&mut self.body),
NewField::Header(i, col) => self.headers.get_mut(i).and_then(|r| r.cell_mut(col)),
NewField::Cookie(i, col) => self.cookies.get_mut(i).and_then(|r| r.cell_mut(col)),
NewField::FormField(i, col) => {
self.form_fields.get_mut(i).and_then(|r| r.cell_mut(col))
}
NewField::Assert(i) => self.asserts.get_mut(i).map(|r| &mut r.expr),
NewField::Capture(i, col) => self.captures.get_mut(i).map(|r| r.cell_mut(col)),
NewField::Method
| NewField::Target
| NewField::AddHeader
| NewField::AddCookie
| NewField::AddFormField
| NewField::AddAssert
| NewField::AddCapture => None,
}
}
pub(crate) fn asserts_blank(&self) -> bool {
self.asserts.iter().all(AssertRow::is_blank)
}
pub(crate) fn captures_blank(&self) -> bool {
self.captures.iter().all(CaptureRow::is_blank)
}
pub(crate) fn assert_entry(&self) -> NewField {
if self.asserts.is_empty() {
NewField::AddAssert
} else {
NewField::Assert(0)
}
}
pub(crate) fn capture_entry(&self) -> NewField {
if self.captures.is_empty() {
NewField::AddCapture
} else {
NewField::Capture(0, CapCol::Name)
}
}
pub(crate) fn prev_cap_col(&self, col: CapCol) -> Option<CapCol> {
match col {
CapCol::Expr => Some(CapCol::Name),
CapCol::Name => None,
}
}
pub(crate) fn next_cap_col(&self, col: CapCol) -> Option<CapCol> {
match col {
CapCol::Name => Some(CapCol::Expr),
CapCol::Expr => None,
}
}
pub(crate) fn headers_blank(&self) -> bool {
self.headers.iter().all(HeaderRow::is_blank)
}
pub(crate) fn cookies_blank(&self) -> bool {
self.cookies.iter().all(HeaderRow::is_blank)
}
pub(crate) fn form_fields_blank(&self) -> bool {
self.form_fields.iter().all(FormRow::is_blank)
}
pub(crate) fn header_entry(&self) -> NewField {
if self.headers.is_empty() {
NewField::AddHeader
} else {
NewField::Header(0, HdrCol::Key)
}
}
pub(crate) fn cookie_entry(&self) -> NewField {
if self.cookies.is_empty() {
NewField::AddCookie
} else {
NewField::Cookie(0, HdrCol::Key)
}
}
pub(crate) fn form_entry(&self) -> NewField {
if self.form_fields.is_empty() {
NewField::AddFormField
} else {
NewField::FormField(0, FormCol::Key)
}
}
pub(crate) fn up_into_headers(&self) -> NewField {
if self.headers.is_empty() {
NewField::AddHeader
} else {
NewField::Header(self.headers.len() - 1, HdrCol::Key)
}
}
pub(crate) fn up_into_cookies(&self) -> NewField {
if self.cookies.is_empty() {
NewField::AddCookie
} else {
NewField::Cookie(self.cookies.len() - 1, HdrCol::Key)
}
}
pub(crate) fn up_into_asserts(&self) -> NewField {
if self.asserts.is_empty() {
NewField::AddAssert
} else {
NewField::Assert(self.asserts.len() - 1)
}
}
fn hdr_row_cells(desc_visible: bool) -> Vec<HdrCol> {
if desc_visible {
vec![HdrCol::Enabled, HdrCol::Key, HdrCol::Value, HdrCol::Desc]
} else {
vec![HdrCol::Enabled, HdrCol::Key, HdrCol::Value]
}
}
fn hdr_tab_cells(desc_visible: bool) -> Vec<HdrCol> {
if desc_visible {
vec![HdrCol::Key, HdrCol::Value, HdrCol::Desc]
} else {
vec![HdrCol::Key, HdrCol::Value]
}
}
pub(crate) fn row_cells(&self) -> Vec<HdrCol> {
Self::hdr_row_cells(self.desc_visible.get())
}
pub(crate) fn tab_cells(&self) -> Vec<HdrCol> {
Self::hdr_tab_cells(self.desc_visible.get())
}
pub(crate) fn prev_col(&self, col: HdrCol) -> Option<HdrCol> {
let cells = self.row_cells();
let idx = cells.iter().position(|c| *c == col)?;
idx.checked_sub(1).map(|p| cells[p])
}
pub(crate) fn next_col(&self, col: HdrCol) -> Option<HdrCol> {
let cells = self.row_cells();
let idx = cells.iter().position(|c| *c == col)?;
cells.get(idx + 1).copied()
}
pub(crate) fn cookie_row_cells(&self) -> Vec<HdrCol> {
Self::hdr_row_cells(self.cookie_desc_visible.get())
}
pub(crate) fn cookie_tab_cells(&self) -> Vec<HdrCol> {
Self::hdr_tab_cells(self.cookie_desc_visible.get())
}
pub(crate) fn prev_cookie_col(&self, col: HdrCol) -> Option<HdrCol> {
let cells = self.cookie_row_cells();
let idx = cells.iter().position(|c| *c == col)?;
idx.checked_sub(1).map(|p| cells[p])
}
pub(crate) fn next_cookie_col(&self, col: HdrCol) -> Option<HdrCol> {
let cells = self.cookie_row_cells();
let idx = cells.iter().position(|c| *c == col)?;
cells.get(idx + 1).copied()
}
pub(crate) fn form_row_cells(&self) -> Vec<FormCol> {
let mut cols = vec![
FormCol::Enabled,
FormCol::Key,
FormCol::Kind,
FormCol::Value,
];
if self.form_ctype_visible.get() {
cols.push(FormCol::Ctype);
}
if self.form_desc_visible.get() {
cols.push(FormCol::Desc);
}
cols
}
pub(crate) fn form_tab_cells(&self) -> Vec<FormCol> {
let mut cols = vec![FormCol::Key, FormCol::Kind, FormCol::Value];
if self.form_ctype_visible.get() {
cols.push(FormCol::Ctype);
}
if self.form_desc_visible.get() {
cols.push(FormCol::Desc);
}
cols
}
pub(crate) fn prev_form_col(&self, col: FormCol) -> Option<FormCol> {
let cells = self.form_row_cells();
let idx = cells.iter().position(|c| *c == col)?;
idx.checked_sub(1).map(|p| cells[p])
}
pub(crate) fn next_form_col(&self, col: FormCol) -> Option<FormCol> {
let cells = self.form_row_cells();
let idx = cells.iter().position(|c| *c == col)?;
cells.get(idx + 1).copied()
}
pub(crate) fn focus_next(&mut self, forward: bool) {
self.focus = if forward {
self.next_forward()
} else {
self.next_backward()
};
}
pub(crate) fn next_forward(&self) -> NewField {
let tab = self.tab_cells();
match self.focus {
NewField::Name => NewField::Target,
NewField::Target => NewField::Method,
NewField::Method => NewField::Url,
NewField::Url => self.header_entry(),
NewField::Header(i, col) => {
if self.headers_blank() {
return self.cookie_entry(); }
let next_in_row = tab
.iter()
.position(|c| *c == col)
.and_then(|idx| tab.get(idx + 1).copied());
if let Some(nc) = next_in_row {
NewField::Header(i, nc)
} else if i + 1 < self.headers.len() {
NewField::Header(i + 1, tab[0])
} else {
NewField::AddHeader
}
}
NewField::AddHeader => self.cookie_entry(),
NewField::Cookie(i, col) => {
if self.cookies_blank() {
return self.form_entry(); }
let ctab = self.cookie_tab_cells();
let next_in_row = ctab
.iter()
.position(|c| *c == col)
.and_then(|idx| ctab.get(idx + 1).copied());
if let Some(nc) = next_in_row {
NewField::Cookie(i, nc)
} else if i + 1 < self.cookies.len() {
NewField::Cookie(i + 1, ctab[0])
} else {
NewField::AddCookie
}
}
NewField::AddCookie => self.form_entry(),
NewField::FormField(i, col) => {
if self.form_fields_blank() {
return NewField::Body; }
let ftab = self.form_tab_cells();
let next_in_row = ftab
.iter()
.position(|c| *c == col)
.and_then(|idx| ftab.get(idx + 1).copied());
if let Some(nc) = next_in_row {
NewField::FormField(i, nc)
} else if i + 1 < self.form_fields.len() {
NewField::FormField(i + 1, ftab[0])
} else {
NewField::AddFormField
}
}
NewField::AddFormField => NewField::Body,
NewField::Body => self.assert_entry(),
NewField::Assert(i) => {
if self.asserts_blank() {
return self.capture_entry(); }
if i + 1 < self.asserts.len() {
NewField::Assert(i + 1)
} else {
NewField::AddAssert
}
}
NewField::AddAssert => self.capture_entry(),
NewField::Capture(i, col) => {
if self.captures_blank() {
return NewField::Name; }
let next_in_row = self.next_cap_col(col);
if let Some(nc) = next_in_row {
NewField::Capture(i, nc)
} else if i + 1 < self.captures.len() {
NewField::Capture(i + 1, CapCol::Name)
} else {
NewField::AddCapture
}
}
NewField::AddCapture => NewField::Name,
}
}
pub(crate) fn next_backward(&self) -> NewField {
let tab = self.tab_cells();
match self.focus {
NewField::Name => {
if self.captures.is_empty() || self.captures_blank() {
if self.asserts.is_empty() || self.asserts_blank() {
NewField::Body
} else {
NewField::Assert(self.asserts.len() - 1)
}
} else {
NewField::AddCapture
}
}
NewField::Target => NewField::Name,
NewField::Method => NewField::Target,
NewField::Url => NewField::Method,
NewField::Header(i, col) => {
let prev_in_row = match tab.iter().position(|c| *c == col) {
Some(idx) => idx.checked_sub(1).map(|p| tab[p]),
None => tab.last().copied(),
};
if let Some(pc) = prev_in_row {
NewField::Header(i, pc)
} else if i > 0 {
NewField::Header(i - 1, *tab.last().unwrap())
} else {
NewField::Url
}
}
NewField::AddHeader => {
if self.headers_blank() || self.headers.is_empty() {
NewField::Url
} else {
NewField::Header(self.headers.len() - 1, *tab.last().unwrap())
}
}
NewField::Cookie(i, col) => {
let ctab = self.cookie_tab_cells();
let prev_in_row = match ctab.iter().position(|c| *c == col) {
Some(idx) => idx.checked_sub(1).map(|p| ctab[p]),
None => ctab.last().copied(),
};
if let Some(pc) = prev_in_row {
NewField::Cookie(i, pc)
} else if i > 0 {
NewField::Cookie(i - 1, *ctab.last().unwrap())
} else {
self.leave_headers_backward()
}
}
NewField::AddCookie => {
if self.cookies_blank() || self.cookies.is_empty() {
self.leave_headers_backward()
} else {
let ctab = self.cookie_tab_cells();
NewField::Cookie(self.cookies.len() - 1, *ctab.last().unwrap())
}
}
NewField::FormField(i, col) => {
let ftab = self.form_tab_cells();
let prev_in_row = match ftab.iter().position(|c| *c == col) {
Some(idx) => idx.checked_sub(1).map(|p| ftab[p]),
None => ftab.last().copied(),
};
if let Some(pc) = prev_in_row {
NewField::FormField(i, pc)
} else if i > 0 {
NewField::FormField(i - 1, *ftab.last().unwrap())
} else {
self.leave_cookies_backward()
}
}
NewField::AddFormField => {
if self.form_fields_blank() || self.form_fields.is_empty() {
self.leave_cookies_backward()
} else {
let ftab = self.form_tab_cells();
NewField::FormField(self.form_fields.len() - 1, *ftab.last().unwrap())
}
}
NewField::Body => self.leave_form_backward(),
NewField::Assert(i) => {
if i > 0 {
NewField::Assert(i - 1)
} else {
NewField::Body
}
}
NewField::AddAssert => {
if self.asserts.is_empty() || self.asserts_blank() {
NewField::Body
} else {
NewField::Assert(self.asserts.len() - 1)
}
}
NewField::Capture(i, col) => match self.prev_cap_col(col) {
Some(pc) => NewField::Capture(i, pc),
None if i > 0 => NewField::Capture(i - 1, CapCol::Expr),
None => {
if self.asserts.is_empty() || self.asserts_blank() {
NewField::Body
} else {
NewField::Assert(self.asserts.len() - 1)
}
}
},
NewField::AddCapture => {
if self.captures.is_empty() || self.captures_blank() {
if self.asserts.is_empty() || self.asserts_blank() {
NewField::Body
} else {
NewField::Assert(self.asserts.len() - 1)
}
} else {
NewField::Capture(self.captures.len() - 1, CapCol::Expr)
}
}
}
}
fn leave_form_backward(&self) -> NewField {
if !self.form_fields.is_empty() && self.form_fields_blank() {
NewField::FormField(0, self.form_tab_cells()[0])
} else {
NewField::AddFormField
}
}
fn leave_cookies_backward(&self) -> NewField {
if !self.cookies.is_empty() && self.cookies_blank() {
NewField::Cookie(0, HdrCol::Key)
} else {
NewField::AddCookie
}
}
fn leave_headers_backward(&self) -> NewField {
if !self.headers.is_empty() && self.headers_blank() {
NewField::Header(0, HdrCol::Key)
} else {
NewField::AddHeader
}
}
pub(crate) fn jump_forward(&self) -> NewField {
match self.focus {
NewField::Name | NewField::Target | NewField::Method | NewField::Url => {
self.next_forward()
}
NewField::Header(..) | NewField::AddHeader => self.cookie_entry(),
NewField::Cookie(..) | NewField::AddCookie => self.form_entry(),
NewField::FormField(..) | NewField::AddFormField => NewField::Body,
NewField::Body => self.assert_entry(),
NewField::Assert(..) | NewField::AddAssert => self.capture_entry(),
NewField::Capture(..) | NewField::AddCapture => NewField::Name,
}
}
pub(crate) fn jump_backward(&self) -> NewField {
match self.focus {
NewField::Name => self.capture_entry(),
NewField::Target | NewField::Method | NewField::Url => self.next_backward(),
NewField::Header(..) | NewField::AddHeader => NewField::Url,
NewField::Cookie(..) | NewField::AddCookie => self.header_entry(),
NewField::FormField(..) | NewField::AddFormField => self.cookie_entry(),
NewField::Body => self.form_entry(),
NewField::Assert(..) | NewField::AddAssert => NewField::Body,
NewField::Capture(..) | NewField::AddCapture => self.assert_entry(),
}
}
pub(crate) fn first_field_of(&self, tab: WizardTab) -> NewField {
match tab {
WizardTab::Headers => self.header_entry(),
WizardTab::Cookies => self.cookie_entry(),
WizardTab::Form => self.form_entry(),
WizardTab::Asserts => self.assert_entry(),
WizardTab::Captures => self.capture_entry(),
other => other.first_field(),
}
}
pub(crate) fn cycle_view_tab(&mut self, forward: bool) {
let len = self.tab_order.len();
if len == 0 {
return;
}
let idx = self
.tab_order
.iter()
.position(|t| *t == self.view_tab)
.unwrap_or(0);
let next = if forward {
(idx + 1) % len
} else {
(idx + len - 1) % len
};
self.view_tab = self.tab_order[next];
if self.view_tab != WizardTab::All {
self.focus = self.first_field_of(self.view_tab);
}
}
pub(crate) fn move_view_tab(&mut self, forward: bool) {
let len = self.tab_order.len();
if len < 2 {
return;
}
let idx = match self.tab_order.iter().position(|t| *t == self.view_tab) {
Some(0) => return, Some(i) => i,
None => return,
};
let target = if forward {
idx + 1
} else {
idx.wrapping_sub(1)
};
if target == 0 || target >= len {
return;
}
self.tab_order.swap(idx, target);
}
}
pub(crate) fn section_height(header_h: u16, row_count: usize) -> u16 {
header_h + (row_count.min(5) as u16) + 1
}
pub(crate) fn draw_new_request(
f: &mut Frame,
form: &NewReq,
s: &Strings,
th: &Theme,
enhanced: bool,
) {
let w = (f.area().width * 7 / 10).max(52);
let h = 36u16.min(f.area().height);
let area = centered_rect(w, h, f.area());
f.render_widget(Clear, area);
let hint_str = if form.editing.is_some() {
s.edit_request_hint
} else {
s.new_request_hint
};
let mut hint = if enhanced {
hint_str.to_string()
} else {
hint_str.replace(&format!("{}/F2", s.ctrl_enter_key), "F2")
};
if let NewField::FormField(i, FormCol::Value) = form.focus
&& form.form_fields.get(i).map(|r| r.kind) == Some(FormFieldKind::File)
{
hint = format!("{hint} · {}", s.hint_pick_file);
}
let on_toggleable_row = matches!(
form.focus,
NewField::Header(i, _) if i < form.headers.len()
) || matches!(
form.focus,
NewField::Cookie(i, _) if i < form.cookies.len()
) || matches!(
form.focus,
NewField::FormField(i, _) if i < form.form_fields.len()
);
if on_toggleable_row {
hint = format!("{hint} · {}", s.hint_toggle_enabled);
}
let on_deletable_row = on_toggleable_row
|| matches!(
form.focus,
NewField::Assert(i) if i < form.asserts.len()
)
|| matches!(
form.focus,
NewField::Capture(i, _) if i < form.captures.len()
);
if on_deletable_row {
hint = format!("{hint} · {}", s.hint_delete_row);
}
let title_text = if form.editing.is_some() {
s.edit_request
} else {
s.new_request
};
let title = format!("{} ({})", title_text, hint);
let block = panel(title, true, th);
let inner = block.inner(area);
f.render_widget(block, area);
let target_h = if form.editing.is_some() { 0 } else { 1 };
let rows = Layout::vertical([
Constraint::Length(1), Constraint::Length(target_h), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Min(0), ])
.split(inner);
new_req_text_row(
f,
rows[0],
s.field_name,
&form.name,
form.focus == NewField::Name,
th,
);
if form.editing.is_none() {
let tcols = Layout::horizontal([Constraint::Length(10), Constraint::Min(1)]).split(rows[1]);
let t_focused = form.focus == NewField::Target;
f.render_widget(
Paragraph::new(Span::styled(
s.field_target.to_string(),
Style::default()
.fg(if t_focused { th.accent } else { th.dim })
.add_modifier(Modifier::BOLD),
)),
tcols[0],
);
f.render_widget(
Paragraph::new(Span::styled(
format!("< {} >", form.target_name()),
Style::default().fg(th.text).add_modifier(Modifier::BOLD),
)),
tcols[1],
);
}
let cols = Layout::horizontal([Constraint::Length(10), Constraint::Min(1)]).split(rows[2]);
let m_focused = form.focus == NewField::Method;
f.render_widget(
Paragraph::new(Span::styled(
s.field_method.to_string(),
Style::default()
.fg(if m_focused { th.accent } else { th.dim })
.add_modifier(Modifier::BOLD),
)),
cols[0],
);
f.render_widget(
Paragraph::new(Span::styled(
format!("< {} >", form.method()),
Style::default()
.fg(method_color(form.method()))
.add_modifier(Modifier::BOLD),
)),
cols[1],
);
draw_url_row(f, rows[3], form, s, th);
draw_wizard_tab_bar(f, rows[4], form.view_tab, &form.tab_order, s, th);
if form.view_tab == WizardTab::All {
let sub = Layout::vertical([
Constraint::Length(1), Constraint::Length(section_height(1, form.headers.len())), Constraint::Length(1), Constraint::Length(section_height(1, form.cookies.len())), Constraint::Length(1), Constraint::Length(section_height(1, form.form_fields.len())), Constraint::Length(1), Constraint::Length(4), Constraint::Length(1), Constraint::Length(section_height(0, form.asserts.len())), Constraint::Length(1), Constraint::Length(section_height(1, form.captures.len())), ])
.split(rows[5]);
draw_headers_section(f, sub[0], sub[1], form, s, th);
draw_cookies_section(f, sub[2], sub[3], form, s, th);
draw_form_section(f, sub[4], sub[5], form, s, th);
draw_body_section(f, sub[6], sub[7], form, s, th);
draw_asserts_section(f, sub[8], sub[9], form, s, th);
draw_captures_section(f, sub[10], sub[11], form, s, th);
} else {
let sub = Layout::vertical([
Constraint::Length(1), Constraint::Min(3), ])
.split(rows[5]);
match form.view_tab {
WizardTab::All => unreachable!(),
WizardTab::Headers => draw_headers_section(f, sub[0], sub[1], form, s, th),
WizardTab::Cookies => draw_cookies_section(f, sub[0], sub[1], form, s, th),
WizardTab::Form => draw_form_section(f, sub[0], sub[1], form, s, th),
WizardTab::Body => draw_body_section(f, sub[0], sub[1], form, s, th),
WizardTab::Asserts => draw_asserts_section(f, sub[0], sub[1], form, s, th),
WizardTab::Captures => draw_captures_section(f, sub[0], sub[1], form, s, th),
}
}
draw_key_suggestions(f, form, s, th);
draw_kind_dropdown(f, form, s, th);
draw_content_type_dropdown(f, form, s, th);
}
fn draw_wizard_tab_bar(
f: &mut Frame,
area: Rect,
active: WizardTab,
order: &[WizardTab],
s: &Strings,
th: &Theme,
) {
let mut spans = Vec::new();
for (i, tab) in order.iter().enumerate() {
if i > 0 {
spans.push(Span::styled(" ", Style::default().fg(th.dim)));
}
let is_active = *tab == active;
let style = if is_active {
Style::default()
.fg(th.bg)
.bg(th.accent)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(th.dim)
};
spans.push(Span::styled(format!(" {} ", tab.label(s)), style));
}
f.render_widget(Paragraph::new(Line::from(spans)), area);
}
fn draw_headers_section(
f: &mut Frame,
label: Rect,
table: Rect,
form: &NewReq,
s: &Strings,
th: &Theme,
) {
let hdr_focused =
matches!(form.focus, NewField::Header(..)) || form.focus == NewField::AddHeader;
f.render_widget(
Paragraph::new(Span::styled(
s.field_headers.to_string(),
Style::default()
.fg(if hdr_focused { th.accent } else { th.dim })
.add_modifier(Modifier::BOLD),
)),
label,
);
draw_header_table(f, table, form, s, th);
}
fn draw_cookies_section(
f: &mut Frame,
label: Rect,
table: Rect,
form: &NewReq,
s: &Strings,
th: &Theme,
) {
let cookie_focused =
matches!(form.focus, NewField::Cookie(..)) || form.focus == NewField::AddCookie;
f.render_widget(
Paragraph::new(Span::styled(
s.field_cookies.to_string(),
Style::default()
.fg(if cookie_focused { th.accent } else { th.dim })
.add_modifier(Modifier::BOLD),
)),
label,
);
draw_cookie_table(f, table, form, s, th);
}
fn draw_form_section(
f: &mut Frame,
label: Rect,
table: Rect,
form: &NewReq,
s: &Strings,
th: &Theme,
) {
let form_focused =
matches!(form.focus, NewField::FormField(..)) || form.focus == NewField::AddFormField;
f.render_widget(
Paragraph::new(Span::styled(
s.field_form.to_string(),
Style::default()
.fg(if form_focused { th.accent } else { th.dim })
.add_modifier(Modifier::BOLD),
)),
label,
);
draw_form_table(f, table, form, s, th);
}
fn draw_body_section(
f: &mut Frame,
label: Rect,
editor: Rect,
form: &NewReq,
s: &Strings,
th: &Theme,
) {
let body_focused = form.focus == NewField::Body;
f.render_widget(
Paragraph::new(Span::styled(
s.field_body.to_string(),
Style::default()
.fg(if body_focused { th.accent } else { th.dim })
.add_modifier(Modifier::BOLD),
)),
label,
);
let total = form.body.lines.len();
let capacity = editor.height as usize;
let scrolling = capacity > 0 && total > capacity;
let text_area = if scrolling {
Rect {
x: editor.x + 1,
y: editor.y,
width: editor.width.saturating_sub(1),
height: editor.height,
}
} else {
editor
};
if body_focused {
render_editor(f, text_area, &form.body, false, th);
} else {
f.render_widget(
Paragraph::new(form.body.text()).style(Style::default().fg(th.text)),
text_area,
);
}
if scrolling {
let start = if body_focused {
form.body.row.saturating_sub(capacity - 1)
} else {
0
};
let bar_area = Rect {
x: editor.x,
y: editor.y,
width: 1,
height: editor.height,
};
draw_scrollbar(f, bar_area, total, capacity, start, th);
}
}
fn draw_asserts_section(
f: &mut Frame,
label: Rect,
table: Rect,
form: &NewReq,
s: &Strings,
th: &Theme,
) {
let assert_focused =
matches!(form.focus, NewField::Assert(_)) || form.focus == NewField::AddAssert;
f.render_widget(
Paragraph::new(Span::styled(
s.field_asserts.to_string(),
Style::default()
.fg(if assert_focused { th.accent } else { th.dim })
.add_modifier(Modifier::BOLD),
)),
label,
);
draw_assert_table(f, table, form, s, th);
}
fn draw_captures_section(
f: &mut Frame,
label: Rect,
table: Rect,
form: &NewReq,
s: &Strings,
th: &Theme,
) {
let cap_focused =
matches!(form.focus, NewField::Capture(..)) || form.focus == NewField::AddCapture;
f.render_widget(
Paragraph::new(Span::styled(
s.field_captures.to_string(),
Style::default()
.fg(if cap_focused { th.accent } else { th.dim })
.add_modifier(Modifier::BOLD),
)),
label,
);
draw_capture_table(f, table, form, s, th);
}
pub(crate) fn draw_kind_dropdown(f: &mut Frame, form: &NewReq, s: &Strings, th: &Theme) {
if !form.kind_dropdown_open() {
return;
}
let NewField::FormField(i, FormCol::Kind) = form.focus else {
return;
};
let Some(row) = form.form_fields.get(i) else {
return;
};
let Some(anchor) = form.kind_cell_rect.get() else {
return;
};
let fr = f.area();
let options = [s.form_type_text, s.form_type_file];
let selected = Some(match row.kind {
FormFieldKind::Text => 0,
FormFieldKind::File => 1,
});
let content_w = options.iter().map(|o| o.len()).max().unwrap_or(0) as u16;
let w = (content_w + 4).max(anchor.width).max(10).min(fr.width);
let h = (options.len() as u16 + 2).min(fr.height);
let mut x = anchor.x;
if x + w > fr.right() {
x = fr.right().saturating_sub(w);
}
let y = if anchor.y + 1 + h <= fr.bottom() {
anchor.y + 1
} else {
anchor.y.saturating_sub(h)
};
let popup = Rect {
x,
y,
width: w,
height: h,
};
f.render_widget(Clear, popup);
let items: Vec<ListItem> = options
.iter()
.map(|opt| {
ListItem::new(Line::styled(
(*opt).to_string(),
Style::default().fg(th.text),
))
})
.collect();
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(th.accent))
.title(Span::styled(
format!(" {} ", s.hdr_type),
Style::default().fg(th.dim),
));
let list = List::new(items)
.block(block)
.highlight_style(
Style::default()
.bg(th.accent)
.fg(th.bg)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol("\u{203a} ");
let mut st = ListState::default();
st.select(selected);
f.render_stateful_widget(list, popup, &mut st);
}
pub(crate) fn draw_content_type_dropdown(f: &mut Frame, form: &NewReq, s: &Strings, th: &Theme) {
if !form.ctype_dropdown_open() {
return;
}
let NewField::FormField(i, FormCol::Ctype) = form.focus else {
return;
};
if form.form_fields.get(i).is_none() {
return;
}
let Some(anchor) = form.ctype_cell_rect.get() else {
return;
};
let fr = f.area();
let entries = form.ctype_dropdown_entries(s);
let labels: Vec<String> = entries
.iter()
.map(|e| {
e.map(|m| m.to_string())
.unwrap_or_else(|| s.content_type_auto.to_string())
})
.collect();
let selected = form.ctype_selected_index(s);
let content_w = labels.iter().map(|o| o.len()).max().unwrap_or(0) as u16;
let w = (content_w + 4).max(anchor.width).max(10).min(fr.width);
let list_h = (labels.len() as u16).min(8);
let h = (list_h + 2).min(fr.height);
let mut x = anchor.x;
if x + w > fr.right() {
x = fr.right().saturating_sub(w);
}
let y = if anchor.y + 1 + h <= fr.bottom() {
anchor.y + 1
} else {
anchor.y.saturating_sub(h)
};
let popup = Rect {
x,
y,
width: w,
height: h,
};
f.render_widget(Clear, popup);
let items: Vec<ListItem> = labels
.iter()
.map(|opt| ListItem::new(Line::styled(opt.clone(), Style::default().fg(th.text))))
.collect();
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(th.accent))
.title(Span::styled(
format!(" {} ", s.content_type_hint),
Style::default().fg(th.dim),
));
let list = List::new(items)
.block(block)
.highlight_style(
Style::default()
.bg(th.accent)
.fg(th.bg)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol("\u{203a} ");
let mut st = ListState::default();
st.select(selected);
f.render_stateful_widget(list, popup, &mut st);
}
pub(crate) fn draw_key_suggestions(f: &mut Frame, form: &NewReq, s: &Strings, th: &Theme) {
let Some((_, sugs)) = form.key_dropdown() else {
return;
};
let Some(anchor) = form.key_cell_rect.get() else {
return;
};
let fr = f.area();
let content_w = sugs.iter().map(|h| h.len()).max().unwrap_or(0) as u16;
let w = (content_w + 4).max(anchor.width).max(16).min(fr.width);
let list_h = (sugs.len() as u16).min(8);
let h = (list_h + 2).min(fr.height);
let mut x = anchor.x;
if x + w > fr.right() {
x = fr.right().saturating_sub(w);
}
let y = if anchor.y + 1 + h <= fr.bottom() {
anchor.y + 1
} else {
anchor.y.saturating_sub(h)
};
let popup = Rect {
x,
y,
width: w,
height: h,
};
f.render_widget(Clear, popup);
let items: Vec<ListItem> = sugs
.iter()
.map(|hname| {
ListItem::new(Line::styled(
(*hname).to_string(),
Style::default().fg(th.text),
))
})
.collect();
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(th.accent))
.title(Span::styled(
format!(" {} ", s.suggest_hint),
Style::default().fg(th.dim),
));
let list = List::new(items)
.block(block)
.highlight_style(
Style::default()
.bg(th.accent)
.fg(th.bg)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol("\u{203a} ");
let mut st = ListState::default();
st.select(form.suggest_hi);
f.render_stateful_widget(list, popup, &mut st);
}
pub(crate) fn scroll_window(
scroll: &std::cell::Cell<usize>,
focused: Option<usize>,
total: usize,
capacity: usize,
) -> usize {
if capacity == 0 || total <= capacity {
scroll.set(0);
return 0;
}
let max_start = total - capacity;
let mut start = scroll.get().min(max_start);
if let Some(idx) = focused {
if idx < start {
start = idx;
} else if idx >= start + capacity {
start = idx + 1 - capacity;
}
}
scroll.set(start);
start
}
pub(crate) fn draw_scrollbar(
f: &mut Frame,
area: Rect,
total: usize,
capacity: usize,
start: usize,
th: &Theme,
) {
if area.width == 0 || area.height == 0 || total <= capacity {
return;
}
let mut state = ScrollbarState::new(total - capacity).position(start);
let bar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(None)
.end_symbol(None)
.track_symbol(Some("\u{2502}"))
.thumb_symbol("\u{2588}")
.style(Style::default().fg(th.dim))
.thumb_style(Style::default().fg(th.accent));
f.render_stateful_widget(bar, area, &mut state);
}
#[allow(clippy::type_complexity)]
fn windowed_table_rows(
area: Rect,
has_header: bool,
data_len: usize,
scroll: &std::cell::Cell<usize>,
focused_data_idx: Option<usize>,
) -> Option<(Rect, Option<Rect>, Vec<Rect>, Rect, bool, usize)> {
if area.height == 0 || area.width == 0 {
return None;
}
let header_h = if has_header { 1usize } else { 0 };
let avail = (area.height as usize).saturating_sub(header_h);
if avail == 0 {
return None;
}
let fits_all = data_len < avail;
let (table_area, scrolling, start, visible) = if fits_all {
scroll.set(0);
(area, false, 0, data_len)
} else {
let table_area = Rect {
x: area.x + 1,
width: area.width.saturating_sub(1),
..area
};
let data_capacity = avail - 1; let start = scroll_window(scroll, focused_data_idx, data_len, data_capacity);
let visible = data_capacity.min(data_len - start);
(table_area, true, start, visible)
};
let mut cons = Vec::new();
if has_header {
cons.push(Constraint::Length(1));
}
cons.extend(std::iter::repeat_n(Constraint::Length(1), visible + 1)); let vlines = Layout::vertical(cons).split(table_area);
let mut idx = 0;
let header_rect = if has_header {
let r = vlines[idx];
idx += 1;
Some(r)
} else {
None
};
let data_rects = (0..visible).map(|k| vlines[idx + k]).collect::<Vec<_>>();
let add_rect = vlines[idx + visible];
Some((
table_area,
header_rect,
data_rects,
add_rect,
scrolling,
start,
))
}
pub(crate) const ENABLED_W: u16 = 3;
pub(crate) fn header_widths(total: u16) -> (u16, u16, u16, u16) {
let en = ENABLED_W;
if total >= en + 18 + 24 + 6 + 3 {
let key = 18;
let value = 24;
let desc = total - en - key - value - 3;
(en, key, value, desc)
} else if total >= en + 4 + 2 {
let rest = total - en - 2; let key = rest / 2;
(en, key, rest - key, 0)
} else {
let rest = total.saturating_sub(en + 2).max(2);
let key = (rest / 2).max(1);
(en, key, rest.saturating_sub(key).max(1), 0)
}
}
pub(crate) fn header_cell_rects(area: Rect, en: u16, kw: u16, vw: u16, dw: u16) -> Vec<Rect> {
let cons: Vec<Constraint> = if dw > 0 {
vec![
Constraint::Length(en),
Constraint::Length(kw),
Constraint::Length(vw),
Constraint::Length(dw),
]
} else {
vec![
Constraint::Length(en),
Constraint::Length(kw),
Constraint::Length(vw),
]
};
Layout::horizontal(cons).spacing(1).split(area).to_vec()
}
pub(crate) fn draw_header_cell(f: &mut Frame, area: Rect, ed: &Editor, focused: bool, th: &Theme) {
if focused {
render_editor(f, area, ed, false, th);
} else {
f.render_widget(
Paragraph::new(ed.text()).style(Style::default().fg(th.text)),
area,
);
}
}
pub(crate) fn draw_checkbox_cell(
f: &mut Frame,
area: Rect,
enabled: bool,
focused: bool,
th: &Theme,
) {
let mark = if enabled { "[x]" } else { "[ ]" };
let mut style = Style::default().fg(if enabled { th.ok } else { th.dim });
if focused {
style = style.add_modifier(Modifier::REVERSED);
}
f.render_widget(Paragraph::new(Span::styled(mark, style)), area);
}
#[allow(clippy::too_many_arguments)]
fn draw_headerlike_table(
f: &mut Frame,
area: Rect,
rows: &[HeaderRow],
desc_visible: &std::cell::Cell<bool>,
scroll: &std::cell::Cell<usize>,
key_cell_rect: Option<&std::cell::Cell<Option<Rect>>>,
focused_idx: Option<usize>,
is_col_focused: impl Fn(usize, HdrCol) -> bool,
add_focused: bool,
add_label: &str,
s: &Strings,
th: &Theme,
) {
if let Some(kcr) = key_cell_rect {
kcr.set(None);
}
let Some((table_area, header_rect, data_rects, add_rect, scrolling, start)) =
windowed_table_rows(area, true, rows.len(), scroll, focused_idx)
else {
return;
};
let (en, kw, vw, dw) = header_widths(table_area.width);
desc_visible.set(dw > 0);
let lbl = |t: &str| {
Paragraph::new(Span::styled(
t.to_string(),
Style::default().fg(th.dim).add_modifier(Modifier::BOLD),
))
};
if let Some(hrect) = header_rect {
let hcells = header_cell_rects(hrect, en, kw, vw, dw);
f.render_widget(lbl("\u{2713}"), hcells[0]); f.render_widget(lbl(s.hdr_key), hcells[1]);
f.render_widget(lbl(s.hdr_value), hcells[2]);
if dw > 0 {
f.render_widget(lbl(s.hdr_description), hcells[3]);
}
}
for (slot, row_area) in data_rects.iter().enumerate() {
let i = start + slot;
let row = &rows[i];
let cells = header_cell_rects(*row_area, en, kw, vw, dw);
draw_checkbox_cell(
f,
cells[0],
row.enabled,
is_col_focused(i, HdrCol::Enabled),
th,
);
draw_header_cell(f, cells[1], &row.key, is_col_focused(i, HdrCol::Key), th);
draw_header_cell(
f,
cells[2],
&row.value,
is_col_focused(i, HdrCol::Value),
th,
);
if dw > 0 {
draw_header_cell(f, cells[3], &row.desc, is_col_focused(i, HdrCol::Desc), th);
}
if is_col_focused(i, HdrCol::Key)
&& let Some(kcr) = key_cell_rect
{
kcr.set(Some(cells[1]));
}
}
f.render_widget(
Paragraph::new(Span::styled(
add_label.to_string(),
Style::default()
.fg(if add_focused { th.accent } else { th.dim })
.add_modifier(Modifier::BOLD),
)),
add_rect,
);
if scrolling {
let header_h = 1u16;
let bar_area = Rect {
x: area.x, y: table_area.y + header_h,
width: 1,
height: table_area.height.saturating_sub(header_h),
};
draw_scrollbar(f, bar_area, rows.len(), data_rects.len().max(1), start, th);
}
}
pub(crate) fn draw_header_table(f: &mut Frame, area: Rect, form: &NewReq, s: &Strings, th: &Theme) {
let focused_idx = match form.focus {
NewField::Header(i, _) => Some(i),
_ => None,
};
draw_headerlike_table(
f,
area,
&form.headers,
&form.desc_visible,
&form.header_scroll,
Some(&form.key_cell_rect),
focused_idx,
|i, col| form.focus == NewField::Header(i, col),
form.focus == NewField::AddHeader,
s.add_header,
s,
th,
);
}
pub(crate) fn draw_cookie_table(f: &mut Frame, area: Rect, form: &NewReq, s: &Strings, th: &Theme) {
let focused_idx = match form.focus {
NewField::Cookie(i, _) => Some(i),
_ => None,
};
draw_headerlike_table(
f,
area,
&form.cookies,
&form.cookie_desc_visible,
&form.cookie_scroll,
None,
focused_idx,
|i, col| form.focus == NewField::Cookie(i, col),
form.focus == NewField::AddCookie,
s.add_cookie,
s,
th,
);
}
pub(crate) fn form_widths(total: u16) -> (u16, u16, u16, u16, u16, u16) {
let en = ENABLED_W;
let kind_w = 8u16;
let ctype_w = 18u16;
let key = 14u16;
let value = 20u16;
if total >= en + kind_w + ctype_w + key + value + 6 + 5 {
let desc = total - en - kind_w - ctype_w - key - value - 5;
(en, key, value, kind_w, ctype_w, desc)
} else if total >= en + kind_w + ctype_w + key + value + 4 {
(en, key, value, kind_w, ctype_w, 0)
} else if total >= en + kind_w + 4 + 3 {
let rest = total - en - kind_w - 3;
let key = (rest / 2).max(1);
(en, key, rest.saturating_sub(key).max(1), kind_w, 0, 0)
} else {
let rest = total.saturating_sub(en + 2).max(2);
let key = (rest / 2).max(1);
(en, key, rest.saturating_sub(key).max(1), 0, 0, 0)
}
}
pub(crate) fn form_cell_rects(
area: Rect,
en: u16,
kw: u16,
vw: u16,
kindw: u16,
ctypew: u16,
dw: u16,
) -> Vec<Rect> {
let mut cons = vec![Constraint::Length(en), Constraint::Length(kw)];
if kindw > 0 {
cons.push(Constraint::Length(kindw));
}
cons.push(Constraint::Length(vw));
if ctypew > 0 {
cons.push(Constraint::Length(ctypew));
}
if dw > 0 {
cons.push(Constraint::Length(dw));
}
Layout::horizontal(cons).spacing(1).split(area).to_vec()
}
fn form_value_color(row: &FormRow, file_root: Option<&PathBuf>, th: &Theme) -> Color {
if row.kind != FormFieldKind::File {
return th.text;
}
let text = row.value.text();
if text.is_empty() {
return th.text;
}
let path = PathBuf::from(text);
let resolved = if path.is_absolute() {
path
} else {
file_root
.cloned()
.unwrap_or_else(|| PathBuf::from("."))
.join(path)
};
if resolved.is_file() { th.ok } else { th.err }
}
pub(crate) fn draw_form_table(f: &mut Frame, area: Rect, form: &NewReq, s: &Strings, th: &Theme) {
form.kind_cell_rect.set(None);
form.ctype_cell_rect.set(None);
let focused_idx = match form.focus {
NewField::FormField(i, _) => Some(i),
_ => None,
};
let Some((table_area, header_rect, data_rects, add_rect, scrolling, start)) =
windowed_table_rows(
area,
true,
form.form_fields.len(),
&form.form_scroll,
focused_idx,
)
else {
return;
};
let (en, kw, vw, kindw, ctypew, dw) = form_widths(table_area.width);
form.form_desc_visible.set(dw > 0);
form.form_ctype_visible.set(ctypew > 0);
let lbl = |t: &str| {
Paragraph::new(Span::styled(
t.to_string(),
Style::default().fg(th.dim).add_modifier(Modifier::BOLD),
))
};
let value_idx = if kindw > 0 { 3 } else { 2 };
let ctype_idx = value_idx + 1;
let desc_idx = ctype_idx + if ctypew > 0 { 1 } else { 0 };
if let Some(hrect) = header_rect {
let hcells = form_cell_rects(hrect, en, kw, vw, kindw, ctypew, dw);
f.render_widget(lbl("\u{2713}"), hcells[0]);
f.render_widget(lbl(s.hdr_key), hcells[1]);
if kindw > 0 {
f.render_widget(lbl(s.hdr_type), hcells[2]);
}
f.render_widget(lbl(s.hdr_value), hcells[value_idx]);
if ctypew > 0 {
f.render_widget(lbl(s.content_type_hint), hcells[ctype_idx]);
}
if dw > 0 {
f.render_widget(lbl(s.hdr_description), hcells[desc_idx]);
}
}
for (slot, row_area) in data_rects.iter().enumerate() {
let i = start + slot;
let row = &form.form_fields[i];
let cells = form_cell_rects(*row_area, en, kw, vw, kindw, ctypew, dw);
draw_checkbox_cell(
f,
cells[0],
row.enabled,
form.focus == NewField::FormField(i, FormCol::Enabled),
th,
);
draw_header_cell(
f,
cells[1],
&row.key,
form.focus == NewField::FormField(i, FormCol::Key),
th,
);
if kindw > 0 {
let kind_focused = form.focus == NewField::FormField(i, FormCol::Kind);
let label = match row.kind {
FormFieldKind::Text => s.form_type_text,
FormFieldKind::File => s.form_type_file,
};
f.render_widget(
Paragraph::new(Span::styled(
format!("{} \u{25be}", label),
Style::default()
.fg(if kind_focused { th.accent } else { th.text })
.add_modifier(Modifier::BOLD),
)),
cells[2],
);
if kind_focused {
form.kind_cell_rect.set(Some(cells[2]));
}
}
let value_focused = form.focus == NewField::FormField(i, FormCol::Value);
let (file_icon_rect, text_rect) =
if row.kind == FormFieldKind::File && cells[value_idx].width > 2 {
let split = Layout::horizontal([Constraint::Length(2), Constraint::Min(1)])
.split(cells[value_idx]);
(Some(split[0]), split[1])
} else {
(None, cells[value_idx])
};
if value_focused {
render_editor(f, text_rect, &row.value, false, th);
} else {
let color = form_value_color(row, form.file_root.as_ref(), th);
f.render_widget(
Paragraph::new(row.value.text()).style(Style::default().fg(color)),
text_rect,
);
}
if let Some(icon_rect) = file_icon_rect {
let icon_color = if value_focused { th.accent } else { th.dim };
f.render_widget(
Paragraph::new(Span::styled(FOLDER_ICON, Style::default().fg(icon_color))),
icon_rect,
);
}
if ctypew > 0 {
let ctype_focused = form.focus == NewField::FormField(i, FormCol::Ctype);
if !ctype_focused
&& row.kind == FormFieldKind::File
&& row.ctype.text().trim().is_empty()
{
f.render_widget(
Paragraph::new(Span::styled(
s.content_type_auto_placeholder,
Style::default().fg(th.dim),
)),
cells[ctype_idx],
);
} else {
draw_header_cell(f, cells[ctype_idx], &row.ctype, ctype_focused, th);
}
if ctype_focused && row.kind == FormFieldKind::File {
form.ctype_cell_rect.set(Some(cells[ctype_idx]));
}
}
if dw > 0 {
let desc_focused = form.focus == NewField::FormField(i, FormCol::Desc);
draw_header_cell(f, cells[desc_idx], &row.desc, desc_focused, th);
}
}
let add_focused = form.focus == NewField::AddFormField;
f.render_widget(
Paragraph::new(Span::styled(
s.add_form_field.to_string(),
Style::default()
.fg(if add_focused { th.accent } else { th.dim })
.add_modifier(Modifier::BOLD),
)),
add_rect,
);
if scrolling {
let header_h = 1u16;
let bar_area = Rect {
x: area.x, y: table_area.y + header_h,
width: 1,
height: table_area.height.saturating_sub(header_h),
};
draw_scrollbar(
f,
bar_area,
form.form_fields.len(),
data_rects.len().max(1),
start,
th,
);
}
}
pub(crate) fn draw_assert_table(f: &mut Frame, area: Rect, form: &NewReq, s: &Strings, th: &Theme) {
let focused_idx = match form.focus {
NewField::Assert(i) => Some(i),
_ => None,
};
let Some((table_area, _, data_rects, add_rect, scrolling, start)) = windowed_table_rows(
area,
false,
form.asserts.len(),
&form.assert_scroll,
focused_idx,
) else {
return;
};
for (slot, row_area) in data_rects.iter().enumerate() {
let i = start + slot;
draw_header_cell(
f,
*row_area,
&form.asserts[i].expr,
form.focus == NewField::Assert(i),
th,
);
}
let add_focused = form.focus == NewField::AddAssert;
f.render_widget(
Paragraph::new(Span::styled(
s.add_assert.to_string(),
Style::default()
.fg(if add_focused { th.accent } else { th.dim })
.add_modifier(Modifier::BOLD),
)),
add_rect,
);
if scrolling {
let bar_area = Rect {
x: area.x,
y: table_area.y,
width: 1,
height: table_area.height,
};
draw_scrollbar(
f,
bar_area,
form.asserts.len(),
data_rects.len().max(1),
start,
th,
);
}
}
pub(crate) fn draw_capture_table(
f: &mut Frame,
area: Rect,
form: &NewReq,
s: &Strings,
th: &Theme,
) {
let focused_idx = match form.focus {
NewField::Capture(i, _) => Some(i),
_ => None,
};
let Some((table_area, header_rect, data_rects, add_rect, scrolling, start)) =
windowed_table_rows(
area,
true,
form.captures.len(),
&form.capture_scroll,
focused_idx,
)
else {
return;
};
let name_w = 18u16.min(table_area.width.saturating_sub(4)).max(4);
let cell_rects = |row_area: Rect| {
Layout::horizontal([Constraint::Length(name_w), Constraint::Min(1)])
.spacing(1)
.split(row_area)
};
let lbl = |t: &str| {
Paragraph::new(Span::styled(
t.to_string(),
Style::default().fg(th.dim).add_modifier(Modifier::BOLD),
))
};
if let Some(hrect) = header_rect {
let hcells = cell_rects(hrect);
f.render_widget(lbl(s.cap_name), hcells[0]);
f.render_widget(lbl(s.cap_expr), hcells[1]);
}
for (slot, row_area) in data_rects.iter().enumerate() {
let i = start + slot;
let row = &form.captures[i];
let cells = cell_rects(*row_area);
draw_header_cell(
f,
cells[0],
&row.name,
form.focus == NewField::Capture(i, CapCol::Name),
th,
);
draw_header_cell(
f,
cells[1],
&row.expr,
form.focus == NewField::Capture(i, CapCol::Expr),
th,
);
}
let add_focused = form.focus == NewField::AddCapture;
f.render_widget(
Paragraph::new(Span::styled(
s.add_capture.to_string(),
Style::default()
.fg(if add_focused { th.accent } else { th.dim })
.add_modifier(Modifier::BOLD),
)),
add_rect,
);
if scrolling {
let header_h = 1u16;
let bar_area = Rect {
x: area.x, y: table_area.y + header_h,
width: 1,
height: table_area.height.saturating_sub(header_h),
};
draw_scrollbar(
f,
bar_area,
form.captures.len(),
data_rects.len().max(1),
start,
th,
);
}
}
pub(crate) fn draw_url_row(f: &mut Frame, area: Rect, form: &NewReq, s: &Strings, th: &Theme) {
let focused = form.focus == NewField::Url;
let cols = Layout::horizontal([Constraint::Length(10), Constraint::Min(1)]).split(area);
f.render_widget(
Paragraph::new(Span::styled(
s.field_url.to_string(),
Style::default()
.fg(if focused { th.accent } else { th.dim })
.add_modifier(Modifier::BOLD),
)),
cols[0],
);
let text = form.url.text();
if text.is_empty() && !form.base_url.is_empty() {
f.render_widget(
Paragraph::new(Span::styled(
form.base_url.clone(),
Style::default().fg(th.dim).add_modifier(Modifier::ITALIC),
)),
cols[1],
);
if focused {
f.set_cursor_position(Position::new(cols[1].x, cols[1].y));
}
} else if focused {
render_editor(f, cols[1], &form.url, false, th);
} else {
f.render_widget(
Paragraph::new(text).style(Style::default().fg(th.text)),
cols[1],
);
}
}
pub(crate) fn new_req_text_row(
f: &mut Frame,
area: Rect,
label: &str,
ed: &Editor,
focused: bool,
th: &Theme,
) {
let cols = Layout::horizontal([Constraint::Length(10), Constraint::Min(1)]).split(area);
f.render_widget(
Paragraph::new(Span::styled(
label.to_string(),
Style::default()
.fg(if focused { th.accent } else { th.dim })
.add_modifier(Modifier::BOLD),
)),
cols[0],
);
if focused {
render_editor(f, cols[1], ed, false, th);
} else {
f.render_widget(
Paragraph::new(ed.text()).style(Style::default().fg(th.text)),
cols[1],
);
}
}