Fix unlocked TUI visibility and clipboard feedback

This commit is contained in:
2026-08-10 18:32:57 +02:00
parent 2eef49c5c3
commit 75d4ead3d8
8 changed files with 673 additions and 288 deletions

View File

@@ -3,18 +3,17 @@
use std::{cell::Cell, fmt};
use ironstorage::{
document::{DocumentError, EntryDocument, EntryField, EntryFieldId, EntrySensitivity},
document::{DocumentError, EntryDocument, EntryField},
repository::SecretBytes,
};
/// Focus, masking, and scrolling state for an authenticated document.
/// Focus and scrolling state for an authenticated document.
///
/// The document remains the source of field order, labels, kinds, sensitivity,
/// and values. This type only tracks transient presentation choices.
pub struct EntryViewer {
document: EntryDocument,
focused: usize,
revealed: Option<EntryFieldId>,
scroll: Cell<usize>,
}
@@ -23,7 +22,6 @@ impl EntryViewer {
Self {
document,
focused: 0,
revealed: None,
scroll: Cell::new(0),
}
}
@@ -44,19 +42,13 @@ impl EntryViewer {
self.document.fields().get(self.focused)
}
pub fn is_revealed(&self, id: EntryFieldId) -> bool {
self.revealed == Some(id)
}
pub fn focus_next(&mut self) {
self.hide_revealed();
if !self.document.fields().is_empty() {
self.focused = (self.focused + 1) % self.document.fields().len();
}
}
pub fn focus_previous(&mut self) {
self.hide_revealed();
if !self.document.fields().is_empty() {
self.focused = self
.focused
@@ -65,21 +57,6 @@ impl EntryViewer {
}
}
pub fn reveal_focused(&mut self) -> bool {
let Some(field) = self.focused_field() else {
return false;
};
if field.metadata().sensitivity() != EntrySensitivity::Sensitive {
return false;
}
self.revealed = Some(field.id());
true
}
pub fn hide_revealed(&mut self) -> bool {
self.revealed.take().is_some()
}
pub fn copy_focused(&self) -> Result<SecretBytes, DocumentError> {
let field = self.focused_field().ok_or(DocumentError::InvalidIndex {
index: self.focused,
@@ -147,7 +124,6 @@ impl fmt::Debug for EntryViewer {
.debug_struct("EntryViewer")
.field("document", &self.document)
.field("focused", &self.focused)
.field("revealed", &self.revealed.map(|_| "[REDACTED]"))
.field("scroll", &self.scroll.get())
.finish()
}
@@ -158,8 +134,10 @@ pub(crate) mod test_support {
use std::path::{Path, PathBuf};
use ironstorage::{
command::GrepRequest,
crypto::{KeyInfo, KeyStore, SecretProvider, SecretProviderError},
document::{EntryDocument, EntryDocumentService},
read::{GrepResults, VaultReader},
repository::{EntryPath, Repository, SecretBytes},
};
use tempfile::TempDir;
@@ -183,6 +161,25 @@ pub(crate) mod test_support {
fixture_document_from(&root.join("stores/basic"), entry)
}
pub(crate) fn fixture_grep() -> GrepResults {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../crates/storage/tests/fixtures/compatibility");
let repository = Repository::open(root.join("stores/basic")).expect("fixture repository");
let keys = KeyStore::load(root.join("keys")).expect("fixture keys");
VaultReader::new(&repository, &keys)
.grep(
&GrepRequest {
pattern: "alice".to_owned(),
ignore_case: false,
invert_match: false,
line_number: true,
fixed_strings: true,
},
&mut FixtureSecrets,
)
.expect("fixture grep")
}
pub(crate) fn fixture_document_from(store: &Path, entry: &str) -> EntryDocument {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../crates/storage/tests/fixtures/compatibility");
@@ -224,14 +221,9 @@ mod tests {
use super::{test_support::fixture_document, *};
#[test]
fn focus_wraps_and_always_hides_a_revealed_secret() {
fn focus_wraps_without_changing_the_authenticated_document() {
let mut viewer = EntryViewer::new(fixture_document("email/personal"));
assert!(viewer.reveal_focused());
let password = viewer.focused_field().expect("password").id();
assert!(viewer.is_revealed(password));
viewer.focus_next();
assert!(!viewer.is_revealed(password));
assert_eq!(viewer.focused_index(), Some(1));
viewer.focus_previous();
assert_eq!(viewer.focused_index(), Some(0));
@@ -247,10 +239,9 @@ mod tests {
}
#[test]
fn ordinary_fields_do_not_gain_reveal_state_and_scroll_saturates() {
fn scrolling_saturates() {
let mut viewer = EntryViewer::new(fixture_document("unicode/咖啡"));
viewer.focus_next();
assert!(!viewer.reveal_focused());
viewer.scroll_down(usize::MAX);
let end = viewer.scroll();
viewer.scroll_down(1);