Implement structured desktop editing
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -4037,6 +4037,8 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"iced",
|
"iced",
|
||||||
"ironstorage",
|
"ironstorage",
|
||||||
|
"tempfile",
|
||||||
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -13,3 +13,7 @@ path = "src/main.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
iced.workspace = true
|
iced.workspace = true
|
||||||
ironstorage.workspace = true
|
ironstorage.workspace = true
|
||||||
|
zeroize.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3"
|
||||||
|
|||||||
154
apps/desktop/src/editor.rs
Normal file
154
apps/desktop/src/editor.rs
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
//! Structured desktop editing state over storage-owned entry documents.
|
||||||
|
|
||||||
|
use std::{collections::BTreeSet, fmt};
|
||||||
|
|
||||||
|
use ironstorage::{
|
||||||
|
document::{
|
||||||
|
DocumentError, EntryDocument, EntryField, EntryFieldDraft, EntryFieldId, EntrySensitivity,
|
||||||
|
},
|
||||||
|
repository::SecretBytes,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct EntryEditor {
|
||||||
|
document: EntryDocument,
|
||||||
|
revealed: BTreeSet<EntryFieldId>,
|
||||||
|
dirty: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EntryEditor {
|
||||||
|
pub fn new(document: EntryDocument) -> Self {
|
||||||
|
Self {
|
||||||
|
document,
|
||||||
|
revealed: BTreeSet::new(),
|
||||||
|
dirty: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn entry(&self) -> String {
|
||||||
|
self.document.path().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn document(&self) -> &EntryDocument {
|
||||||
|
&self.document
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fields(&self) -> &[EntryField] {
|
||||||
|
self.document.fields()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_dirty(&self) -> bool {
|
||||||
|
self.dirty
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_revealed(&self, id: EntryFieldId) -> bool {
|
||||||
|
self.revealed.contains(&id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_reveal(&mut self, id: EntryFieldId) -> Result<(), DocumentError> {
|
||||||
|
let field = self
|
||||||
|
.document
|
||||||
|
.field(id)
|
||||||
|
.ok_or(DocumentError::UnknownField { id })?;
|
||||||
|
if field.metadata().sensitivity() != EntrySensitivity::Sensitive {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if !self.revealed.remove(&id) {
|
||||||
|
self.revealed.insert(id);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_raw(&mut self, id: EntryFieldId, value: &[u8]) -> Result<(), DocumentError> {
|
||||||
|
let unchanged = self
|
||||||
|
.document
|
||||||
|
.field(id)
|
||||||
|
.is_some_and(|field| field.contents().expose() == value);
|
||||||
|
if unchanged {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
self.document
|
||||||
|
.update(id, EntryFieldDraft::line(value.to_vec())?)?;
|
||||||
|
self.dirty = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_after(&mut self, id: Option<EntryFieldId>) -> Result<(), DocumentError> {
|
||||||
|
let index = id
|
||||||
|
.and_then(|id| {
|
||||||
|
self.document
|
||||||
|
.fields()
|
||||||
|
.iter()
|
||||||
|
.position(|field| field.id() == id)
|
||||||
|
})
|
||||||
|
.map_or(self.document.fields().len(), |index| index + 1);
|
||||||
|
self.document.add(index, EntryFieldDraft::blank())?;
|
||||||
|
self.dirty = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, id: EntryFieldId) -> Result<(), DocumentError> {
|
||||||
|
self.document.remove(id)?;
|
||||||
|
self.revealed.remove(&id);
|
||||||
|
self.dirty = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_up(&mut self, id: EntryFieldId) -> Result<(), DocumentError> {
|
||||||
|
let Some(index) = self
|
||||||
|
.document
|
||||||
|
.fields()
|
||||||
|
.iter()
|
||||||
|
.position(|field| field.id() == id)
|
||||||
|
else {
|
||||||
|
return Err(DocumentError::UnknownField { id });
|
||||||
|
};
|
||||||
|
if index > 0 {
|
||||||
|
self.document.reorder(id, index - 1)?;
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_down(&mut self, id: EntryFieldId) -> Result<(), DocumentError> {
|
||||||
|
let Some(index) = self
|
||||||
|
.document
|
||||||
|
.fields()
|
||||||
|
.iter()
|
||||||
|
.position(|field| field.id() == id)
|
||||||
|
else {
|
||||||
|
return Err(DocumentError::UnknownField { id });
|
||||||
|
};
|
||||||
|
if index + 1 < self.document.fields().len() {
|
||||||
|
self.document.reorder(id, index + 1)?;
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn replace_value(
|
||||||
|
&mut self,
|
||||||
|
id: EntryFieldId,
|
||||||
|
value: SecretBytes,
|
||||||
|
) -> Result<(), DocumentError> {
|
||||||
|
self.document
|
||||||
|
.replace_field_value(id, value.expose().to_vec())?;
|
||||||
|
self.revealed.remove(&id);
|
||||||
|
self.dirty = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_value(&self, id: EntryFieldId) -> Result<SecretBytes, DocumentError> {
|
||||||
|
self.document.copy_field_value(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for EntryEditor {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
formatter
|
||||||
|
.debug_struct("EntryEditor")
|
||||||
|
.field("document", &self.document)
|
||||||
|
.field("revealed", &self.revealed)
|
||||||
|
.field("dirty", &self.dirty)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user