Implement TUI search and mutation workflows
This commit is contained in:
@@ -4,7 +4,10 @@ use std::{fmt, num::NonZeroUsize};
|
||||
|
||||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
use ironstorage::{
|
||||
command::{GenerateRequest, GeneratedPresentation, InitRequest, InsertInput, InsertRequest},
|
||||
command::{
|
||||
CopyRequest, GenerateRequest, GeneratedPresentation, GrepRequest, InitRequest, InsertInput,
|
||||
InsertRequest, MoveRequest, RemoveRequest,
|
||||
},
|
||||
crypto::KeyInfo,
|
||||
write::{InsertContent, OverwriteDecision},
|
||||
};
|
||||
@@ -104,11 +107,43 @@ pub struct GenerateForm {
|
||||
focus: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GrepForm {
|
||||
pattern: String,
|
||||
ignore_case: bool,
|
||||
invert_match: bool,
|
||||
line_number: bool,
|
||||
fixed_strings: bool,
|
||||
focus: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RemoveForm {
|
||||
target: String,
|
||||
recursive: bool,
|
||||
force: bool,
|
||||
confirmed: bool,
|
||||
focus: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TransferForm {
|
||||
source: String,
|
||||
destination: String,
|
||||
destination_directory: bool,
|
||||
overwrite: bool,
|
||||
focus: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum WorkflowForm {
|
||||
Init(InitForm),
|
||||
Insert(InsertForm),
|
||||
Generate(GenerateForm),
|
||||
Grep(GrepForm),
|
||||
Remove(RemoveForm),
|
||||
Move(TransferForm),
|
||||
Copy(TransferForm),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -123,6 +158,16 @@ pub enum WorkflowSubmission {
|
||||
request: GenerateRequest,
|
||||
overwrite: OverwriteDecision,
|
||||
},
|
||||
Grep(GrepRequest),
|
||||
Remove(RemoveRequest),
|
||||
Move {
|
||||
request: MoveRequest,
|
||||
overwrite: OverwriteDecision,
|
||||
},
|
||||
Copy {
|
||||
request: CopyRequest,
|
||||
overwrite: OverwriteDecision,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
@@ -215,11 +260,74 @@ impl WorkflowForm {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn grep(request: Option<GrepRequest>) -> Self {
|
||||
let request = request.unwrap_or(GrepRequest {
|
||||
pattern: String::new(),
|
||||
ignore_case: false,
|
||||
invert_match: false,
|
||||
line_number: true,
|
||||
fixed_strings: false,
|
||||
});
|
||||
Self::Grep(GrepForm {
|
||||
pattern: request.pattern,
|
||||
ignore_case: request.ignore_case,
|
||||
invert_match: request.invert_match,
|
||||
line_number: request.line_number,
|
||||
fixed_strings: request.fixed_strings,
|
||||
focus: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remove(request: Option<RemoveRequest>, selected: Option<(&str, bool)>) -> Self {
|
||||
let request = request.unwrap_or_else(|| RemoveRequest {
|
||||
entry: selected.map_or_else(String::new, |(path, _)| path.to_owned()),
|
||||
recursive: selected.is_some_and(|(_, directory)| directory),
|
||||
force: false,
|
||||
});
|
||||
Self::Remove(RemoveForm {
|
||||
target: request.entry,
|
||||
recursive: request.recursive,
|
||||
force: request.force,
|
||||
confirmed: false,
|
||||
focus: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_entry(request: Option<MoveRequest>, selected: Option<&str>) -> Self {
|
||||
let request = request.unwrap_or_else(|| MoveRequest {
|
||||
source: selected.unwrap_or_default().to_owned(),
|
||||
destination: String::new(),
|
||||
force: false,
|
||||
});
|
||||
Self::Move(transfer_form(
|
||||
request.source,
|
||||
request.destination,
|
||||
request.force,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn copy_entry(request: Option<CopyRequest>, selected: Option<&str>) -> Self {
|
||||
let request = request.unwrap_or_else(|| CopyRequest {
|
||||
source: selected.unwrap_or_default().to_owned(),
|
||||
destination: String::new(),
|
||||
force: false,
|
||||
});
|
||||
Self::Copy(transfer_form(
|
||||
request.source,
|
||||
request.destination,
|
||||
request.force,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn title(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Init(_) => "Initialize recipients",
|
||||
Self::Insert(_) => "Insert entry",
|
||||
Self::Generate(_) => "Generate password",
|
||||
Self::Grep(_) => "Search decrypted entries",
|
||||
Self::Remove(_) => "Remove entry or folder",
|
||||
Self::Move(_) => "Move or rename",
|
||||
Self::Copy(_) => "Copy entry or folder",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,6 +393,37 @@ impl WorkflowForm {
|
||||
row(form.focus == 3, "Overwrite", yes_no(form.overwrite)),
|
||||
row(form.focus == 4, "In place", yes_no(form.in_place)),
|
||||
],
|
||||
Self::Grep(form) => vec![
|
||||
row(form.focus == 0, "Pattern", &form.pattern),
|
||||
row(form.focus == 1, "Ignore case", yes_no(form.ignore_case)),
|
||||
row(form.focus == 2, "Invert match", yes_no(form.invert_match)),
|
||||
row(form.focus == 3, "Line numbers", yes_no(form.line_number)),
|
||||
row(form.focus == 4, "Fixed string", yes_no(form.fixed_strings)),
|
||||
],
|
||||
Self::Remove(form) => vec![
|
||||
row(form.focus == 0, "Target", &form.target),
|
||||
row(form.focus == 1, "Recursive folder", yes_no(form.recursive)),
|
||||
row(form.focus == 2, "Force", yes_no(form.force)),
|
||||
row(
|
||||
form.focus == 3,
|
||||
"Confirm permanent removal",
|
||||
yes_no(form.confirmed),
|
||||
),
|
||||
],
|
||||
Self::Move(form) | Self::Copy(form) => vec![
|
||||
row(form.focus == 0, "Source", &form.source),
|
||||
row(form.focus == 1, "Destination", &form.destination),
|
||||
row(
|
||||
form.focus == 2,
|
||||
"Destination is existing directory",
|
||||
yes_no(form.destination_directory),
|
||||
),
|
||||
row(
|
||||
form.focus == 3,
|
||||
"Overwrite collision",
|
||||
yes_no(form.overwrite),
|
||||
),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,6 +526,53 @@ impl WorkflowForm {
|
||||
},
|
||||
})
|
||||
}
|
||||
Self::Grep(form) => {
|
||||
if form.pattern.is_empty() {
|
||||
return Err("Search pattern is required".to_owned());
|
||||
}
|
||||
Ok(WorkflowSubmission::Grep(GrepRequest {
|
||||
pattern: form.pattern.clone(),
|
||||
ignore_case: form.ignore_case,
|
||||
invert_match: form.invert_match,
|
||||
line_number: form.line_number,
|
||||
fixed_strings: form.fixed_strings,
|
||||
}))
|
||||
}
|
||||
Self::Remove(form) => {
|
||||
if form.target.trim().is_empty() {
|
||||
return Err("Removal target is required".to_owned());
|
||||
}
|
||||
if !form.confirmed {
|
||||
return Err("Explicitly confirm permanent removal".to_owned());
|
||||
}
|
||||
Ok(WorkflowSubmission::Remove(RemoveRequest {
|
||||
entry: form.target.trim().to_owned(),
|
||||
recursive: form.recursive,
|
||||
force: form.force,
|
||||
}))
|
||||
}
|
||||
Self::Move(form) => {
|
||||
let (source, destination, overwrite) = transfer_submission(form)?;
|
||||
Ok(WorkflowSubmission::Move {
|
||||
request: MoveRequest {
|
||||
source,
|
||||
destination,
|
||||
force: form.overwrite,
|
||||
},
|
||||
overwrite,
|
||||
})
|
||||
}
|
||||
Self::Copy(form) => {
|
||||
let (source, destination, overwrite) = transfer_submission(form)?;
|
||||
Ok(WorkflowSubmission::Copy {
|
||||
request: CopyRequest {
|
||||
source,
|
||||
destination,
|
||||
force: form.overwrite,
|
||||
},
|
||||
overwrite,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,6 +580,8 @@ impl WorkflowForm {
|
||||
match self {
|
||||
Self::Init(form) => form.recipients.len() + 1,
|
||||
Self::Insert(_) | Self::Generate(_) => 5,
|
||||
Self::Grep(_) => 5,
|
||||
Self::Remove(_) | Self::Move(_) | Self::Copy(_) => 4,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,6 +591,9 @@ impl WorkflowForm {
|
||||
Self::Init(form) => &mut form.focus,
|
||||
Self::Insert(form) => &mut form.focus,
|
||||
Self::Generate(form) => &mut form.focus,
|
||||
Self::Grep(form) => &mut form.focus,
|
||||
Self::Remove(form) => &mut form.focus,
|
||||
Self::Move(form) | Self::Copy(form) => &mut form.focus,
|
||||
};
|
||||
*focus = if forward {
|
||||
(*focus + 1) % count
|
||||
@@ -429,6 +620,17 @@ impl WorkflowForm {
|
||||
form.overwrite = false;
|
||||
}
|
||||
}
|
||||
Self::Grep(form) if form.focus == 1 => form.ignore_case ^= true,
|
||||
Self::Grep(form) if form.focus == 2 => form.invert_match ^= true,
|
||||
Self::Grep(form) if form.focus == 3 => form.line_number ^= true,
|
||||
Self::Grep(form) if form.focus == 4 => form.fixed_strings ^= true,
|
||||
Self::Remove(form) if form.focus == 1 => form.recursive ^= true,
|
||||
Self::Remove(form) if form.focus == 2 => form.force ^= true,
|
||||
Self::Remove(form) if form.focus == 3 => form.confirmed ^= true,
|
||||
Self::Move(form) | Self::Copy(form) if form.focus == 2 => {
|
||||
form.destination_directory ^= true
|
||||
}
|
||||
Self::Move(form) | Self::Copy(form) if form.focus == 3 => form.overwrite ^= true,
|
||||
_ => self.character(' '),
|
||||
}
|
||||
}
|
||||
@@ -460,6 +662,18 @@ impl WorkflowForm {
|
||||
Self::Generate(form) if form.focus == 1 => {
|
||||
form.length.pop();
|
||||
}
|
||||
Self::Grep(form) if form.focus == 0 => {
|
||||
form.pattern.pop();
|
||||
}
|
||||
Self::Remove(form) if form.focus == 0 => {
|
||||
form.target.pop();
|
||||
}
|
||||
Self::Move(form) | Self::Copy(form) if form.focus == 0 => {
|
||||
form.source.pop();
|
||||
}
|
||||
Self::Move(form) | Self::Copy(form) if form.focus == 1 => {
|
||||
form.destination.pop();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -482,11 +696,53 @@ impl WorkflowForm {
|
||||
Self::Generate(form) if form.focus == 1 && character.is_ascii_digit() => {
|
||||
form.length.push(character)
|
||||
}
|
||||
Self::Grep(form) if form.focus == 0 => form.pattern.push(character),
|
||||
Self::Remove(form) if form.focus == 0 => form.target.push(character),
|
||||
Self::Move(form) | Self::Copy(form) if form.focus == 0 => form.source.push(character),
|
||||
Self::Move(form) | Self::Copy(form) if form.focus == 1 => {
|
||||
form.destination.push(character)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn transfer_form(source: String, mut destination: String, overwrite: bool) -> TransferForm {
|
||||
let destination_directory = destination.ends_with('/');
|
||||
while destination.ends_with('/') {
|
||||
destination.pop();
|
||||
}
|
||||
TransferForm {
|
||||
source,
|
||||
destination,
|
||||
destination_directory,
|
||||
overwrite,
|
||||
focus: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn transfer_submission(form: &TransferForm) -> Result<(String, String, OverwriteDecision), String> {
|
||||
if form.source.trim().is_empty() {
|
||||
return Err("Source path is required".to_owned());
|
||||
}
|
||||
if form.destination.trim().is_empty() {
|
||||
return Err("Destination path is required".to_owned());
|
||||
}
|
||||
let mut destination = form.destination.trim().to_owned();
|
||||
if form.destination_directory {
|
||||
destination.push('/');
|
||||
}
|
||||
Ok((
|
||||
form.source.trim().to_owned(),
|
||||
destination,
|
||||
if form.overwrite {
|
||||
OverwriteDecision::Allow
|
||||
} else {
|
||||
OverwriteDecision::Decline
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn row(focused: bool, label: &str, value: &str) -> String {
|
||||
format!("{} {label}: {value}", if focused { ">" } else { " " })
|
||||
}
|
||||
@@ -567,4 +823,89 @@ mod tests {
|
||||
_ => panic!("wrong submission"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_and_mutation_forms_preserve_every_explicit_storage_choice() {
|
||||
let grep = WorkflowForm::grep(Some(GrepRequest {
|
||||
pattern: "literal.*value".to_owned(),
|
||||
ignore_case: true,
|
||||
invert_match: true,
|
||||
line_number: true,
|
||||
fixed_strings: true,
|
||||
}));
|
||||
assert!(matches!(
|
||||
grep.submission(),
|
||||
Ok(WorkflowSubmission::Grep(GrepRequest {
|
||||
ignore_case: true,
|
||||
invert_match: true,
|
||||
line_number: true,
|
||||
fixed_strings: true,
|
||||
..
|
||||
}))
|
||||
));
|
||||
|
||||
let mut remove = WorkflowForm::remove(
|
||||
Some(RemoveRequest {
|
||||
entry: "nested/folder".to_owned(),
|
||||
recursive: true,
|
||||
force: true,
|
||||
}),
|
||||
None,
|
||||
);
|
||||
assert!(remove.submission().unwrap_err().contains("confirm"));
|
||||
for _ in 0..3 {
|
||||
remove.handle_key(KeyCode::Tab, KeyModifiers::NONE);
|
||||
}
|
||||
remove.handle_key(KeyCode::Char(' '), KeyModifiers::NONE);
|
||||
assert!(matches!(
|
||||
remove.submission(),
|
||||
Ok(WorkflowSubmission::Remove(RemoveRequest {
|
||||
recursive: true,
|
||||
force: true,
|
||||
..
|
||||
}))
|
||||
));
|
||||
|
||||
let copied = WorkflowForm::copy_entry(
|
||||
Some(CopyRequest {
|
||||
source: "source/entry".to_owned(),
|
||||
destination: "existing/folder/".to_owned(),
|
||||
force: true,
|
||||
}),
|
||||
None,
|
||||
);
|
||||
assert!(
|
||||
copied
|
||||
.rows()
|
||||
.join("\n")
|
||||
.contains("Destination is existing directory: yes")
|
||||
);
|
||||
assert!(matches!(
|
||||
copied.submission(),
|
||||
Ok(WorkflowSubmission::Copy {
|
||||
request: CopyRequest {
|
||||
ref destination,
|
||||
force: true,
|
||||
..
|
||||
},
|
||||
overwrite: OverwriteDecision::Allow,
|
||||
}) if destination == "existing/folder/"
|
||||
));
|
||||
|
||||
let moved = WorkflowForm::move_entry(
|
||||
Some(MoveRequest {
|
||||
source: "old".to_owned(),
|
||||
destination: "new".to_owned(),
|
||||
force: false,
|
||||
}),
|
||||
None,
|
||||
);
|
||||
assert!(matches!(
|
||||
moved.submission(),
|
||||
Ok(WorkflowSubmission::Move {
|
||||
overwrite: OverwriteDecision::Decline,
|
||||
..
|
||||
})
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user