use super::*;
pub(super) const PROVIDER_ROW_REFUSAL: &str = "read-only: pushed by a dog, not set here";
impl App {
pub(super) fn refuse_provider_row(&mut self, row: &SecretRow) -> bool {
if matches!(row.source, Source::Namespace(_)) {
self.notice = Some(Notice::grave(PROVIDER_ROW_REFUSAL.to_string()));
return true;
}
false
}
}
pub(super) const ALL_SLOT_REFUSAL: &str = "this value comes from the `all` slot: removing it \
affects every environment, so switch to the `all` tab to mean it";
pub(super) const NOTHING_REMOVED: &str =
"nothing to remove: this key holds no value in this environment";
pub(super) fn deletes_the_all_slot(row: &SecretRow, environment: &str) -> bool {
row.in_force.as_deref() == Some(ALL_ENVIRONMENTS) && environment != ALL_ENVIRONMENTS
}
pub(super) const NEW_KEY_GRAMMAR: &str =
"letters, digits, `.`, `_` and `-`, up to 128 bytes, not starting with a dot";
pub(super) const COPY_SENT_NOTICE: &str = "sent to the terminal's clipboard over OSC 52 \u{b7} readable there by every process on the desktop";
pub(crate) const REVEAL_HOLDS: Duration = Duration::from_secs(10);
pub(crate) struct SecretsPane {
pub model: Box<SecretsModel>,
pub tab: usize,
pub selected: usize,
pub collapsed: HashSet<String>,
pub reveal: Option<Reveal>,
pub pending_reveal: Option<String>,
pub armed: Option<ArmedDelete>,
pub typing: Option<Typing>,
}
#[derive(Debug, Clone)]
pub(crate) struct ArmedDelete {
pub key: String,
pub at: Instant,
}
impl SecretsPane {
pub(crate) fn hide(&mut self) {
self.reveal = None;
self.pending_reveal = None;
}
pub(crate) fn environment(&self) -> Option<&str> {
self.model.environments.get(self.tab).map(String::as_str)
}
pub(crate) fn is_collapsed(&self, source: &Source) -> bool {
match source {
Source::Operator => false,
Source::Namespace(namespace) => self.collapsed.contains(namespace),
}
}
pub(super) fn visible_row_indices(&self) -> Vec<usize> {
self.model
.rows
.iter()
.enumerate()
.filter(|(_, row)| !self.is_collapsed(&row.source))
.map(|(index, _)| index)
.collect()
}
pub(super) fn selected_visible_row(&self) -> Option<SecretRow> {
if !self.visible_row_indices().contains(&self.selected) {
return None;
}
self.model.rows.get(self.selected).cloned()
}
pub(crate) fn selected_is_new_key_row(&self) -> bool {
self.selected == self.model.rows.len()
}
pub(crate) fn new_key_anchor(&self) -> Option<usize> {
self.model
.rows
.iter()
.enumerate()
.filter(|(_, row)| row.source == Source::Operator)
.map(|(index, _)| index)
.max()
}
fn screen_slots(&self) -> Vec<Option<usize>> {
let anchor = self.new_key_anchor();
let visible = self.visible_row_indices();
let insert_at = match anchor {
Some(anchor) => visible
.iter()
.position(|&index| index > anchor)
.unwrap_or(visible.len()),
None => 0,
};
let mut slots: Vec<Option<usize>> = visible.into_iter().map(Some).collect();
slots.insert(insert_at, None);
slots
}
pub(crate) fn move_by(&mut self, delta: isize) {
let slots = self.screen_slots();
if slots.is_empty() {
return;
}
let current = (!self.selected_is_new_key_row()).then_some(self.selected);
if let Some(position) = slots.iter().position(|&slot| slot == current) {
let next = position.saturating_add_signed(delta).min(slots.len() - 1);
self.selected = slots[next].unwrap_or(self.model.rows.len());
return;
}
let visible = self.visible_row_indices();
if visible.is_empty() {
return;
}
let boundary = visible.partition_point(|&index| index < self.selected);
self.selected = if delta < 0 {
visible[boundary.saturating_sub(1).min(visible.len() - 1)]
} else {
visible[boundary.min(visible.len() - 1)]
};
}
pub(crate) fn move_to_first(&mut self) {
if let Some(&slot) = self.screen_slots().first() {
self.selected = slot.unwrap_or(self.model.rows.len());
}
}
pub(crate) fn move_to_last(&mut self) {
if let Some(&slot) = self.screen_slots().last() {
self.selected = slot.unwrap_or(self.model.rows.len());
}
}
}
impl fmt::Debug for SecretsPane {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SecretsPane")
.field("rows", &self.model.rows.len())
.field("tab", &self.tab)
.field("selected", &self.selected)
.field("collapsed", &self.collapsed.len())
.field("revealing", &self.reveal.is_some())
.field("pending_reveal", &self.pending_reveal)
.field("armed", &self.armed.as_ref().map(|a| &a.key))
.field("typing", &self.typing.is_some())
.finish()
}
}
pub(crate) struct Reveal {
pub key: String,
pub value: String,
pub until: Instant,
}
impl fmt::Debug for Reveal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Reveal")
.field("key", &self.key)
.field("value", &format_args!("<{} bytes>", self.value.len()))
.finish_non_exhaustive()
}
}
#[derive(Clone)]
pub struct RevealedValue(pub(crate) String);
impl fmt::Debug for RevealedValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "RevealedValue(<{} bytes>)", self.0.len())
}
}
#[derive(Clone, PartialEq, Eq)]
pub(crate) struct ClipboardValue(pub(crate) String);
impl fmt::Debug for ClipboardValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ClipboardValue(<{} bytes>)", self.0.len())
}
}
#[derive(Clone, PartialEq, Eq)]
pub(crate) struct SecretEdit {
pub key: String,
pub environment: String,
pub value: Option<String>,
}
impl fmt::Debug for SecretEdit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let value = self.value.as_ref().map_or_else(
|| "None".to_string(),
|v| format!("Some(<{} bytes>)", v.len()),
);
f.debug_struct("SecretEdit")
.field("key", &self.key)
.field("environment", &self.environment)
.field("value", &format_args!("{value}"))
.finish()
}
}
pub(crate) struct Typing {
pub what: TypingWhat,
pub buffer: TextBox,
}
impl fmt::Debug for Typing {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Typing")
.field("what", &self.what)
.field(
"buffer",
&format_args!("<{} bytes>", self.buffer.as_str().len()),
)
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum TypingWhat {
NewKey,
ValueFor(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_pane_debug_never_prints_a_revealed_value() {
let pane = SecretsPane {
model: Box::default(),
tab: 0,
selected: 0,
collapsed: HashSet::new(),
reveal: Some(Reveal {
key: "K".into(),
value: "hunter2".into(),
until: Instant::now(),
}),
pending_reveal: None,
armed: None,
typing: Some(Typing {
what: TypingWhat::ValueFor("K".into()),
buffer: "hunter2".into(),
}),
};
let printed = format!("{pane:?}");
assert_eq!(
printed,
"SecretsPane { rows: 0, tab: 0, selected: 0, collapsed: 0, \
revealing: true, pending_reveal: None, armed: None, typing: true }"
);
assert!(!format!("{:?}", pane.reveal).contains("hunter2"));
assert!(!format!("{:?}", pane.typing).contains("hunter2"));
}
#[test]
fn a_secret_edit_debug_prints_a_length_and_never_the_value() {
let edit = SecretEdit {
key: "DB_PASSWORD".into(),
environment: "production".into(),
value: Some("hunter2".into()),
};
assert_eq!(
format!("{edit:?}"),
"SecretEdit { key: \"DB_PASSWORD\", environment: \"production\", \
value: Some(<7 bytes>) }"
);
}
#[test]
fn the_msg_debug_never_prints_a_revealed_value() {
let landed = Msg::Revealed {
key: "K".to_string(),
environment: "production".to_string(),
value: Some(RevealedValue("hunter2".to_string())),
};
let printed = format!("{landed:?}");
assert_eq!(
printed,
"Revealed { key: \"K\", environment: \"production\", \
value: Some(RevealedValue(<7 bytes>)) }"
);
}
#[test]
fn the_effect_debug_never_prints_a_copied_value() {
let effect = Effect::CopyToClipboard(ClipboardValue("hunter2".to_string()));
assert_eq!(
format!("{effect:?}"),
"CopyToClipboard(ClipboardValue(<7 bytes>))"
);
}
}