use super::super::super::app::{REVEAL_HOLDS, Reveal, SecretsPane, TypingWhat};
use super::super::super::secrets::SecretRow;
use super::super::super::textbox::TextBox;
use super::super::super::theme::Palette;
use super::super::flock::{fit, fit_owned};
use super::column_tiers::Column;
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use std::time::Instant;
pub(super) fn byte_count(len: usize) -> String {
if len == 1 {
"1 byte".to_string()
} else {
format!("{len} bytes")
}
}
pub(super) fn typed_cell(buffer: &TextBox, width: u16) -> String {
buffer.drawn(width.saturating_sub(1))
}
pub(super) fn value_cell(
row: &SecretRow,
revealed: Option<&Reveal>,
typing: Option<&TextBox>,
width: u16,
) -> String {
if let Some(buffer) = typing {
return fit(&typed_cell(buffer, width), width);
}
if let Some(reveal) = revealed {
return fit(&reveal.value, width);
}
let Some(len) = row.byte_len else {
return "not set here".to_string();
};
let suffix = format!(" {}", byte_count(len));
let run = usize::from(width)
.saturating_sub(suffix.len())
.saturating_sub(1)
.min(len);
format!("{}{suffix}", "█".repeat(run.max(1)))
}
fn set_in_cell(row: &SecretRow, environment_count: usize) -> String {
if row.set_in.is_empty() {
return "-".to_string();
}
let count = row.set_in.len();
if environment_count > 0 && count >= environment_count {
format!("{count} of {environment_count}")
} else {
format!(
"{count} of {environment_count} \u{b7} {}",
row.set_in.join(", ")
)
}
}
const GAUGE_CELLS: usize = 10;
pub(super) fn countdown(until: Instant, now: Instant) -> String {
let left = until
.saturating_duration_since(now)
.as_millis()
.div_ceil(1000);
let hold = u128::from(REVEAL_HOLDS.as_secs()).max(1);
let cells = u128::try_from(GAUGE_CELLS).unwrap_or(0);
let filled = usize::try_from(left * cells / hold)
.unwrap_or(0)
.min(GAUGE_CELLS);
format!(
"visible {left}s {}{}",
"\u{2588}".repeat(filled),
"\u{2591}".repeat(GAUGE_CELLS - filled)
)
}
fn row_cell(
row: &SecretRow,
column: Column,
revealed: Option<&Reveal>,
typing: Option<&TextBox>,
environment_count: usize,
now: Instant,
) -> String {
match column {
Column::Key => row.key.clone(),
Column::Value => value_cell(row, revealed, typing, column.width()),
Column::InForce => row.in_force.clone().unwrap_or_else(|| "-".to_string()),
Column::SetIn => set_in_cell(row, environment_count),
Column::ReadBy => {
if row.readers.is_empty() {
"-".to_string()
} else {
let online = row.readers.iter().filter(|reader| reader.online).count();
format!("{} ({online} online)", row.readers.len())
}
}
Column::Lands => {
revealed.map_or_else(|| "-".to_string(), |reveal| countdown(reveal.until, now))
}
}
}
pub(super) fn row_line(
pane: &SecretsPane,
row: &SecretRow,
columns: &[Column],
width: u16,
palette: Palette,
selected: bool,
now: Instant,
) -> Line<'static> {
let ground = if selected {
palette.ground()
} else {
Style::default()
};
let revealed = pane.reveal.as_ref().filter(|reveal| reveal.key == row.key);
let typing = if selected {
pane.typing.as_ref().and_then(|typing| match &typing.what {
TypingWhat::ValueFor(key) if key == &row.key => Some(&typing.buffer),
TypingWhat::ValueFor(_) | TypingWhat::NewKey => None,
})
} else {
None
};
let environment_count = pane.model.environments.len();
let mut spans: Vec<Span<'static>> = Vec::with_capacity(columns.len());
let mut used = 0u16;
for column in columns {
let text = fit_owned(
row_cell(row, *column, revealed, typing, environment_count, now),
column.width(),
);
spans.push(Span::styled(text, ground));
used += column.width();
}
pad(&mut spans, used, width, ground);
Line::from(spans)
}
pub(super) fn pad(spans: &mut Vec<Span<'static>>, used: u16, width: u16, style: Style) {
let short = width.saturating_sub(used);
if short > 0 {
spans.push(Span::styled(" ".repeat(usize::from(short)), style));
}
}
pub(super) fn new_key_row_line(
pane: &SecretsPane,
columns: &[Column],
width: u16,
palette: Palette,
selected: bool,
) -> Line<'static> {
let ground = if selected {
palette.ground()
} else {
Style::default()
};
let typing_name = selected
.then_some(pane.typing.as_ref())
.flatten()
.and_then(|typing| matches!(typing.what, TypingWhat::NewKey).then_some(&typing.buffer));
let mut spans: Vec<Span<'static>> = Vec::with_capacity(columns.len());
let mut used = 0u16;
for column in columns {
let text = match column {
Column::Key => "+ new key".to_string(),
Column::Value => {
typing_name.map_or_else(String::new, |buffer| typed_cell(buffer, column.width()))
}
Column::InForce | Column::SetIn | Column::ReadBy | Column::Lands => String::new(),
};
spans.push(Span::styled(fit(&text, column.width()), ground));
used += column.width();
}
pad(&mut spans, used, width, ground);
Line::from(spans)
}
#[cfg(test)]
mod tests {
use super::super::column_tiers::Column;
use super::super::draw_layout::cell;
use super::super::pane_chrome::tab_line;
use super::super::super::super::theme::Palette;
use super::*;
use super::super::testing::*;
use crate::lookout::view::fixtures;
#[test]
fn the_elision_marker_stands_on_the_side_the_tabs_went() {
let palette = Palette::detect(None, None, None);
let head = fixtures::rendered(&tab_line(&pane_with_environments(20, 0), palette, 100));
let tail = fixtures::rendered(&tab_line(&pane_with_environments(20, 19), palette, 100));
assert!(
!head.trim_start().starts_with('\u{2026}'),
"nothing is hidden before the first tab: {head:?}"
);
assert!(head.contains('\u{2026}'), "and plenty after it: {head:?}");
assert!(
tail.trim_start().starts_with('\u{2026}'),
"hidden tabs before the last one: {tail:?}"
);
}
#[test]
fn a_one_byte_value_is_one_byte_in_both_places_that_print_a_length() {
assert_eq!(byte_count(1), "1 byte");
assert_eq!(byte_count(0), "0 bytes");
assert_eq!(byte_count(9), "9 bytes");
}
#[test]
fn a_provider_group_says_it_is_read_only() {
let app = fixtures::app_with_a_pushed_secret();
let buffer = fixtures::render(&app, 160, 48);
let text = fixtures::rows_of(&buffer);
assert!(
text.iter().any(|line| line.contains("read-only here")),
"the group header states it: {text:?}"
);
}
#[test]
fn a_key_set_everywhere_names_no_environments() {
let app = fixtures::app_with_secrets();
let buffer = fixtures::render(&app, 160, 48);
let header_count = header_environment_count(&buffer);
let row = row_of(&buffer, "SET_IN_ALL_THREE");
assert_eq!(
cell(&buffer, row, Column::SetIn).trim(),
format!("{header_count} of {header_count}")
);
}
}