use ratatui::style::Style;
use ratatui::text::{Line, Span};
use super::super::super::pane::{ConfigPane, EnvTyping, PaneRow};
use super::super::super::theme::Palette;
use super::super::flock::{fit, mark};
use super::chrome::pending_edit_line;
use super::field_row::section_header;
use super::layout::{body_width, widths};
pub(super) fn pending_and_env_lines(
pane: &ConfigPane,
palette: Palette,
width: u16,
budget: usize,
show_lands: bool,
) -> Vec<Line<'static>> {
let mut lines: Vec<Line<'static>> = Vec::new();
if budget == 0 {
return lines;
}
let pending_len = if pane.edits().is_empty() {
0
} else {
2 + pane.edits().len()
};
let env_len = 2 + pane.env_key_names().len() + 1;
if pending_len + env_len > budget
&& let Some(PaneRow::Env(_) | PaneRow::AddEnv) = pane.cursor()
{
return vec![cursor_env_row_line(pane, width, palette, show_lands)];
}
let mut remaining = budget;
if !pane.edits().is_empty() && remaining > 0 {
lines.push(Line::default());
remaining -= 1;
if remaining > 0 {
lines.push(section_header("pending edits", palette));
remaining -= 1;
}
for (_, entry) in pane.edits().iter() {
if remaining == 0 {
break;
}
lines.push(pending_edit_line(pane, entry.edit(), width, palette));
remaining -= 1;
}
}
if remaining > 0 {
lines.push(Line::default());
remaining -= 1;
}
if remaining > 0 {
lines.push(section_header("env", palette));
remaining -= 1;
}
for (index, name) in pane.env_key_names().iter().enumerate() {
if remaining == 0 {
break;
}
let selected = pane.cursor() == Some(PaneRow::Env(index));
lines.push(env_row_line(
pane, name, selected, width, palette, show_lands,
));
remaining -= 1;
}
if remaining > 0 {
let selected = pane.cursor() == Some(PaneRow::AddEnv);
lines.push(add_env_row_line(pane, selected, width, palette, show_lands));
}
lines
}
pub(super) fn cursor_env_row_line(
pane: &ConfigPane,
width: u16,
palette: Palette,
show_lands: bool,
) -> Line<'static> {
match pane.cursor() {
Some(PaneRow::Env(index)) => {
let name = pane
.env_key_names()
.get(index)
.expect("the cursor names a real env row");
env_row_line(pane, name, true, width, palette, show_lands)
}
Some(PaneRow::AddEnv) => add_env_row_line(pane, true, width, palette, show_lands),
Some(PaneRow::Field(_)) | None => {
unreachable!("callers check the cursor is on an env row first")
}
}
}
pub(super) fn env_row_line(
pane: &ConfigPane,
name: &str,
selected: bool,
width: u16,
palette: Palette,
show_lands: bool,
) -> Line<'static> {
let (key_w, value_w, _) = widths(body_width(width), show_lands);
let typing = selected
.then(|| pane.env_typing())
.flatten()
.filter(|typing| typing.key() == Some(name))
.map(EnvTyping::buffer);
let value = typing.map_or_else(|| "(set)".to_owned(), |buffer| format!("{buffer}\u{258f}"));
let ground = if selected {
palette.ground()
} else {
Style::default()
};
let mark_style = if selected {
palette.attention().patch(ground)
} else {
Style::default()
};
let mark_span = Span::styled(mark(selected), mark_style);
let mut text = String::from(" ");
text.push_str(&fit(&format!(" {name}"), key_w));
if value_w > 0 {
text.push_str(" ");
text.push_str(&fit(&value, value_w));
}
Line::from(vec![mark_span, Span::styled(text, ground)])
}
pub(super) fn add_env_row_line(
pane: &ConfigPane,
selected: bool,
width: u16,
palette: Palette,
show_lands: bool,
) -> Line<'static> {
let (key_w, _, _) = widths(body_width(width), show_lands);
let typing = selected
.then(|| pane.env_typing())
.flatten()
.filter(|typing| typing.key().is_none())
.map(EnvTyping::buffer);
let ground = if selected {
palette.ground()
} else {
Style::default()
};
let mark_style = if selected {
palette.attention().patch(ground)
} else {
Style::default()
};
let mark_span = Span::styled(mark(selected), mark_style);
let (caption, muted) = match typing {
Some(buffer) => (format!("{buffer}\u{258f}"), false),
None => ("+ add a key".to_owned(), true),
};
let mut text = String::from(" ");
text.push_str(&fit(&format!(" {caption}"), key_w));
let style = if muted {
palette.muted()
} else {
Style::default()
};
Line::from(vec![mark_span, Span::styled(text, style.patch(ground))])
}
#[cfg(test)]
mod tests {
use super::super::fixtures::{text_of, web_pane};
use super::super::pane_lines;
use super::*;
use crate::lookout::view::MIN_TERM_WIDTH;
use crate::lookout::view::fixtures;
use crate::output::width::visible_width;
#[test]
fn the_env_rows_draw_at_a_comfortable_width() {
let pane = web_pane();
let rows = fixtures::config_pane_env_rows_for_tests(&pane);
assert!(rows.iter().any(|row| row.contains("DB_HOST")), "{rows:?}");
assert!(rows.iter().any(|row| row.contains("LOG_LEVEL")), "{rows:?}");
assert!(
rows.iter()
.all(|row| row.contains("(set)") || row.contains("+ add a key")),
"{rows:?}"
);
assert!(
rows.iter().any(|row| row.contains("+ add a key")),
"{rows:?}"
);
}
#[test]
fn a_filed_env_write_never_reaches_the_screen() {
let mut pane = web_pane();
pane.move_to_last();
assert_eq!(pane.cursor(), Some(PaneRow::AddEnv));
pane.begin_env_typing();
for typed in "NEW_KEY=hunter2".chars() {
pane.type_env_char(typed);
}
pane.apply_env_typing();
let text = text_of(&pane_lines(&pane, fixtures::plain(), 120, 0));
assert!(!text.join("\n").contains("hunter2"), "{text:?}");
}
#[test]
fn every_env_value_renders_as_set_and_never_as_itself() {
let pane = ConfigPane::sheep({
let mut config = shep_core::config::AppConfig {
name: "web".to_string(),
..Default::default()
};
config
.env
.insert("DB_PASSWORD".to_string(), "hunter2".to_string());
shep_core::protocol::SheepConfigView::new(config, Vec::new(), Vec::new())
});
let rows = fixtures::config_pane_env_rows_for_tests(&pane);
assert!(
rows.iter().any(|row| row.contains("DB_PASSWORD")),
"{rows:?}"
);
assert!(
rows.iter().all(|row| !row.contains("hunter2")),
"an env value reached the pane: {rows:?}"
);
assert!(rows.iter().any(|row| row.contains("(set)")), "{rows:?}");
}
#[test]
fn every_env_line_fits_the_width_it_was_drawn_for() {
let mut pane = web_pane();
pane.move_to_last();
assert_eq!(pane.cursor(), Some(PaneRow::AddEnv));
pane.begin_env_typing();
for typed in "A_VERY_LONG_ENV_KEY_NAME=and a value longer still".chars() {
pane.type_env_char(typed);
}
for width in MIN_TERM_WIDTH..=200 {
for line in text_of(&pane_lines(&pane, fixtures::plain(), width, 0)) {
assert!(
visible_width(&line) <= usize::from(width),
"width {width} drew {}: {line:?}",
visible_width(&line)
);
}
}
}
}