Fix TUI field visibility and contrast
This commit is contained in:
@@ -20,6 +20,8 @@ const MINIMUM_WIDTH: u16 = 40;
|
|||||||
const MINIMUM_HEIGHT: u16 = 8;
|
const MINIMUM_HEIGHT: u16 = 8;
|
||||||
const LARGE_OTP_HEIGHT: u16 = 8;
|
const LARGE_OTP_HEIGHT: u16 = 8;
|
||||||
const LARGE_OTP_ROWS: usize = 5;
|
const LARGE_OTP_ROWS: usize = 5;
|
||||||
|
const SELECTED_FOREGROUND: Color = Color::Black;
|
||||||
|
const SELECTED_BACKGROUND: Color = Color::White;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum ColorCapability {
|
pub enum ColorCapability {
|
||||||
@@ -72,7 +74,9 @@ pub fn draw_with_color_capability(frame: &mut Frame, app: &App, capability: Colo
|
|||||||
draw_inner(frame, app);
|
draw_inner(frame, app);
|
||||||
if capability == ColorCapability::Monochrome {
|
if capability == ColorCapability::Monochrome {
|
||||||
for cell in &mut frame.buffer_mut().content {
|
for cell in &mut frame.buffer_mut().content {
|
||||||
if cell.fg == Color::White && cell.bg == Color::Blue {
|
if (cell.fg == SELECTED_FOREGROUND && cell.bg == SELECTED_BACKGROUND)
|
||||||
|
|| (cell.fg == Color::White && cell.bg == Color::Blue)
|
||||||
|
{
|
||||||
cell.modifier.insert(Modifier::REVERSED);
|
cell.modifier.insert(Modifier::REVERSED);
|
||||||
}
|
}
|
||||||
cell.set_fg(Color::Reset).set_bg(Color::Reset);
|
cell.set_fg(Color::Reset).set_bg(Color::Reset);
|
||||||
@@ -560,9 +564,12 @@ fn pane_block(title: &'static str, focused: bool) -> Block<'static> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn selected_line<'a>(mut spans: Vec<Span<'a>>) -> Line<'a> {
|
fn selected_line<'a>(mut spans: Vec<Span<'a>>) -> Line<'a> {
|
||||||
let style = Style::default().bg(Color::Blue).fg(Color::White);
|
let style = Style::default()
|
||||||
|
.fg(SELECTED_FOREGROUND)
|
||||||
|
.bg(SELECTED_BACKGROUND)
|
||||||
|
.add_modifier(Modifier::BOLD);
|
||||||
for span in &mut spans {
|
for span in &mut spans {
|
||||||
span.style = span.style.bg(Color::Blue).fg(Color::White);
|
span.style = style;
|
||||||
}
|
}
|
||||||
Line::from(spans).style(style)
|
Line::from(spans).style(style)
|
||||||
}
|
}
|
||||||
@@ -984,7 +991,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::app::Transition;
|
use crate::app::Transition;
|
||||||
use crate::sidebar::TestTreeNode;
|
use crate::sidebar::TestTreeNode;
|
||||||
use crate::viewer::test_support::fixture_document;
|
use crate::viewer::test_support::{fixture_document, fixture_document_from_plaintext};
|
||||||
|
|
||||||
fn render(width: u16, height: u16, app: &App) -> String {
|
fn render(width: u16, height: u16, app: &App) -> String {
|
||||||
let backend = TestBackend::new(width, height);
|
let backend = TestBackend::new(width, height);
|
||||||
@@ -1373,6 +1380,60 @@ mod tests {
|
|||||||
assert!(!format!("{app:?}").contains("pässwörd-猫"));
|
assert!(!format!("{app:?}").contains("pässwörd-猫"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn authenticated_viewer_renders_ordinary_metadata_and_rehides_secrets() {
|
||||||
|
let (_store, document) = fixture_document_from_plaintext(
|
||||||
|
"metadata/account",
|
||||||
|
b"vault-secret\nautoType_enabled: true\nicon: Internet\nicon: Custom Icon\ntitle: Caf\xc3\xa9\ncustom: hidden-value\n",
|
||||||
|
);
|
||||||
|
let mut app = App::new();
|
||||||
|
app.sidebar_mut().replace_test_tree(vec![TestTreeNode {
|
||||||
|
path: "metadata/account".to_owned(),
|
||||||
|
name: "account".to_owned(),
|
||||||
|
directory: false,
|
||||||
|
indicators: ironstorage::read::TreeNodeIndicators::default(),
|
||||||
|
children: vec![],
|
||||||
|
}]);
|
||||||
|
assert!(matches!(
|
||||||
|
app.dispatch(crate::action::Action::Activate),
|
||||||
|
crate::app::AppEffect::AuthenticateEntry(entry) if entry == "metadata/account"
|
||||||
|
));
|
||||||
|
assert!(app.authentication_granted("metadata/account".to_owned()));
|
||||||
|
let token = app.begin_request();
|
||||||
|
assert_eq!(
|
||||||
|
app.apply_result(crate::app::AsyncResult {
|
||||||
|
token,
|
||||||
|
payload: Ok(crate::app::AsyncPayload::DocumentLoaded {
|
||||||
|
entry: "metadata/account".to_owned(),
|
||||||
|
document: Box::new(document),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
crate::app::ResultDisposition::Applied
|
||||||
|
);
|
||||||
|
|
||||||
|
let unlocked = render(140, 20, &app);
|
||||||
|
assert!(unlocked.contains("autoType_enabled: true"));
|
||||||
|
assert!(unlocked.contains("icon: Internet"));
|
||||||
|
assert!(unlocked.contains("icon: Custom Icon"));
|
||||||
|
assert!(unlocked.contains("title: Café"));
|
||||||
|
assert!(unlocked.contains("password: ••••••••"));
|
||||||
|
assert!(unlocked.contains("custom: ••••••••"));
|
||||||
|
assert!(!unlocked.contains("vault-secret"));
|
||||||
|
assert!(!unlocked.contains("hidden-value"));
|
||||||
|
|
||||||
|
app.dispatch(crate::action::Action::Reveal);
|
||||||
|
assert!(render(140, 20, &app).contains("vault-secret"));
|
||||||
|
app.dispatch(crate::action::Action::FocusNext);
|
||||||
|
assert!(!render(140, 20, &app).contains("vault-secret"));
|
||||||
|
|
||||||
|
app.dispatch(crate::action::Action::FocusPrevious);
|
||||||
|
app.dispatch(crate::action::Action::Reveal);
|
||||||
|
app.forced_relock("Authentication expired");
|
||||||
|
let locked = render(140, 20, &app);
|
||||||
|
assert!(!locked.contains("vault-secret"));
|
||||||
|
assert!(!locked.contains("autoType_enabled: true"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reveal_is_explicit_and_focus_change_or_lock_removes_secret_from_rendering() {
|
fn reveal_is_explicit_and_focus_change_or_lock_removes_secret_from_rendering() {
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
@@ -1591,13 +1652,17 @@ mod tests {
|
|||||||
.any(|span| span.content.contains("code 123456"))
|
.any(|span| span.content.contains("code 123456"))
|
||||||
);
|
);
|
||||||
assert!(selected.spans.iter().all(|span| {
|
assert!(selected.spans.iter().all(|span| {
|
||||||
span.style.fg == Some(Color::White) && span.style.bg == Some(Color::Blue)
|
span.style.fg == Some(SELECTED_FOREGROUND)
|
||||||
|
&& span.style.bg == Some(SELECTED_BACKGROUND)
|
||||||
|
&& span.style.add_modifier.contains(Modifier::BOLD)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let editor = EntryEditor::new(fixture_document("email/personal"));
|
let editor = EntryEditor::new(fixture_document("email/personal"));
|
||||||
let editor_selected = &editor_lines(&editor)[0];
|
let editor_selected = &editor_lines(&editor)[0];
|
||||||
assert!(editor_selected.spans.iter().all(|span| {
|
assert!(editor_selected.spans.iter().all(|span| {
|
||||||
span.style.fg == Some(Color::White) && span.style.bg == Some(Color::Blue)
|
span.style.fg == Some(SELECTED_FOREGROUND)
|
||||||
|
&& span.style.bg == Some(SELECTED_BACKGROUND)
|
||||||
|
&& span.style.add_modifier.contains(Modifier::BOLD)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let backend = TestBackend::new(60, 16);
|
let backend = TestBackend::new(60, 16);
|
||||||
|
|||||||
@@ -160,8 +160,9 @@ pub(crate) mod test_support {
|
|||||||
use ironstorage::{
|
use ironstorage::{
|
||||||
crypto::{KeyInfo, KeyStore, SecretProvider, SecretProviderError},
|
crypto::{KeyInfo, KeyStore, SecretProvider, SecretProviderError},
|
||||||
document::{EntryDocument, EntryDocumentService},
|
document::{EntryDocument, EntryDocumentService},
|
||||||
repository::{Repository, SecretBytes},
|
repository::{EntryPath, Repository, SecretBytes},
|
||||||
};
|
};
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
struct FixtureSecrets;
|
struct FixtureSecrets;
|
||||||
|
|
||||||
@@ -191,6 +192,31 @@ pub(crate) mod test_support {
|
|||||||
.open(entry, &mut FixtureSecrets)
|
.open(entry, &mut FixtureSecrets)
|
||||||
.expect("fixture document")
|
.expect("fixture document")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fixture_document_from_plaintext(
|
||||||
|
entry: &str,
|
||||||
|
plaintext: &[u8],
|
||||||
|
) -> (TempDir, EntryDocument) {
|
||||||
|
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("../../crates/storage/tests/fixtures/compatibility");
|
||||||
|
let store = tempfile::tempdir().expect("temporary fixture store");
|
||||||
|
let repository = Repository::open(store.path()).expect("fixture repository");
|
||||||
|
let keys = KeyStore::load(root.join("keys")).expect("fixture keys");
|
||||||
|
let recipients = keys
|
||||||
|
.resolve_recipients(b"7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30\n")
|
||||||
|
.expect("fixture recipient");
|
||||||
|
let ciphertext = keys
|
||||||
|
.encrypt(SecretBytes::new(plaintext.to_vec()), &recipients)
|
||||||
|
.expect("fixture encryption");
|
||||||
|
let path = EntryPath::parse(entry).expect("fixture entry path");
|
||||||
|
repository
|
||||||
|
.write_entry(&path, &ciphertext)
|
||||||
|
.expect("fixture entry write");
|
||||||
|
let document = EntryDocumentService::new(&repository, &keys)
|
||||||
|
.open(entry, &mut FixtureSecrets)
|
||||||
|
.expect("fixture document");
|
||||||
|
(store, document)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -643,7 +643,7 @@ fn field_sensitivity(name: &str, kind: EntryFieldKind) -> EntrySensitivity {
|
|||||||
return EntrySensitivity::Ordinary;
|
return EntrySensitivity::Ordinary;
|
||||||
}
|
}
|
||||||
match name.trim().to_ascii_lowercase().as_str() {
|
match name.trim().to_ascii_lowercase().as_str() {
|
||||||
"title" | "site" | "host" => EntrySensitivity::Ordinary,
|
"title" | "site" | "host" | "autotype_enabled" | "icon" => EntrySensitivity::Ordinary,
|
||||||
_ => EntrySensitivity::Sensitive,
|
_ => EntrySensitivity::Sensitive,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ fn complex_documents_round_trip_with_storage_owned_metadata() -> TestResult {
|
|||||||
let repository = Repository::open(store.path())?;
|
let repository = Repository::open(store.path())?;
|
||||||
let keys = KeyStore::load(fixture.path("keys"))?;
|
let keys = KeyStore::load(fixture.path("keys"))?;
|
||||||
let mut secrets = FixtureSecrets::all(&fixture);
|
let mut secrets = FixtureSecrets::all(&fixture);
|
||||||
let plaintext = b"p\xc3\xa4ssw\xc3\xb6rd\r\nusername: alice\r\ncustom: one\r\ncustom: \r\notpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP&issuer=Example\r\n\r\nfirst note\r\nsecond note\r\n\xe9\x8d\xb5: \xe5\x80\xbc\r\nunrecognized line";
|
let plaintext = b"p\xc3\xa4ssw\xc3\xb6rd\r\nusername: alice\r\nautoType_enabled: true\r\nicon: Internet\r\ncustom: one\r\ncustom: \r\notpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP&issuer=Example\r\n\r\nfirst note\r\nsecond note\r\n\xe9\x8d\xb5: \xe5\x80\xbc\r\nunrecognized line";
|
||||||
write_plaintext(&repository, &keys, "documents/complex", plaintext)?;
|
write_plaintext(&repository, &keys, "documents/complex", plaintext)?;
|
||||||
|
|
||||||
let service = EntryDocumentService::new(&repository, &keys);
|
let service = EntryDocumentService::new(&repository, &keys);
|
||||||
@@ -79,7 +79,7 @@ fn complex_documents_round_trip_with_storage_owned_metadata() -> TestResult {
|
|||||||
document.password().expect("password").value(),
|
document.password().expect("password").value(),
|
||||||
"pässwörd".as_bytes()
|
"pässwörd".as_bytes()
|
||||||
);
|
);
|
||||||
assert_eq!(document.fields().len(), 10);
|
assert_eq!(document.fields().len(), 12);
|
||||||
|
|
||||||
let kinds = document
|
let kinds = document
|
||||||
.fields()
|
.fields()
|
||||||
@@ -93,6 +93,8 @@ fn complex_documents_round_trip_with_storage_owned_metadata() -> TestResult {
|
|||||||
EntryFieldKind::Username,
|
EntryFieldKind::Username,
|
||||||
EntryFieldKind::Field,
|
EntryFieldKind::Field,
|
||||||
EntryFieldKind::Field,
|
EntryFieldKind::Field,
|
||||||
|
EntryFieldKind::Field,
|
||||||
|
EntryFieldKind::Field,
|
||||||
EntryFieldKind::OtpUri,
|
EntryFieldKind::OtpUri,
|
||||||
EntryFieldKind::Blank,
|
EntryFieldKind::Blank,
|
||||||
EntryFieldKind::Note,
|
EntryFieldKind::Note,
|
||||||
@@ -106,18 +108,29 @@ fn complex_documents_round_trip_with_storage_owned_metadata() -> TestResult {
|
|||||||
document.fields()[1].metadata().sensitivity(),
|
document.fields()[1].metadata().sensitivity(),
|
||||||
EntrySensitivity::Ordinary
|
EntrySensitivity::Ordinary
|
||||||
);
|
);
|
||||||
assert_eq!(document.fields()[2].metadata().name(), Some("custom"));
|
for (index, name, value) in [
|
||||||
assert_eq!(document.fields()[2].value(), b"one");
|
(2, "autoType_enabled", b"true".as_slice()),
|
||||||
assert_eq!(document.fields()[3].metadata().name(), Some("custom"));
|
(3, "icon", b"Internet".as_slice()),
|
||||||
assert!(document.fields()[3].value().is_empty());
|
] {
|
||||||
assert_ne!(document.fields()[2].id(), document.fields()[3].id());
|
assert_eq!(document.fields()[index].metadata().name(), Some(name));
|
||||||
assert_eq!(document.fields()[8].metadata().name(), Some("鍵"));
|
assert_eq!(document.fields()[index].value(), value);
|
||||||
assert_eq!(document.fields()[8].value(), "值".as_bytes());
|
assert_eq!(
|
||||||
|
document.fields()[index].metadata().sensitivity(),
|
||||||
|
EntrySensitivity::Ordinary
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(document.fields()[4].metadata().name(), Some("custom"));
|
||||||
|
assert_eq!(document.fields()[4].value(), b"one");
|
||||||
|
assert_eq!(document.fields()[5].metadata().name(), Some("custom"));
|
||||||
|
assert!(document.fields()[5].value().is_empty());
|
||||||
|
assert_ne!(document.fields()[4].id(), document.fields()[5].id());
|
||||||
|
assert_eq!(document.fields()[10].metadata().name(), Some("鍵"));
|
||||||
|
assert_eq!(document.fields()[10].value(), "值".as_bytes());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
document.fields()[4].metadata().sensitivity(),
|
document.fields()[6].metadata().sensitivity(),
|
||||||
EntrySensitivity::Sensitive
|
EntrySensitivity::Sensitive
|
||||||
);
|
);
|
||||||
let otp = document.fields()[4]
|
let otp = document.fields()[6]
|
||||||
.metadata()
|
.metadata()
|
||||||
.otp()
|
.otp()
|
||||||
.expect("validated OTP metadata");
|
.expect("validated OTP metadata");
|
||||||
@@ -128,10 +141,10 @@ fn complex_documents_round_trip_with_storage_owned_metadata() -> TestResult {
|
|||||||
assert_eq!(otp.digits(), 6);
|
assert_eq!(otp.digits(), 6);
|
||||||
assert_eq!(otp.period(), Some(30));
|
assert_eq!(otp.period(), Some(30));
|
||||||
assert_eq!(otp.counter(), None);
|
assert_eq!(otp.counter(), None);
|
||||||
let copied = document.copy_field_value(document.fields()[2].id())?;
|
let copied = document.copy_field_value(document.fields()[4].id())?;
|
||||||
assert_eq!(copied.expose(), b"one");
|
assert_eq!(copied.expose(), b"one");
|
||||||
assert!(!format!("{document:?}").contains("pässwörd"));
|
assert!(!format!("{document:?}").contains("pässwörd"));
|
||||||
assert!(!format!("{:?}", document.fields()[4]).contains("JBSWY3DPEHPK3PXP"));
|
assert!(!format!("{:?}", document.fields()[6]).contains("JBSWY3DPEHPK3PXP"));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user