commands/list: debounce for rapid copy operations

Tracks the entry ID currently being copied in `TuiState` to prevent
concurrent `copy_entry()` calls on the same entity. Otherwise we hit a
race condition. Fun!


Track the entry ID currently being copied in TuiState to prevent
concurrent copy_entry() calls on the same entry. Fixes database
race conditions when users trigger copy commands in rapid succession.



Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: If8e8fe56bf6dc35960e47decf59636116a6a6964
This commit is contained in:
raf 2026-03-05 14:06:12 +03:00
commit 0865a1f139
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF

View file

@ -57,6 +57,9 @@ struct TuiState {
/// Whether to show entries in reverse order (oldest first). /// Whether to show entries in reverse order (oldest first).
reverse: bool, reverse: bool,
/// ID of entry currently being copied.
copying_entry: Option<i64>,
} }
impl TuiState { impl TuiState {
@ -91,6 +94,7 @@ impl TuiState {
search_query: String::new(), search_query: String::new(),
search_mode: false, search_mode: false,
reverse, reverse,
copying_entry: None,
}) })
} }
@ -678,42 +682,51 @@ impl SqliteClipboardDb {
if actions.copy if actions.copy
&& let Some(&(id, ..)) = tui.selected_entry() && let Some(&(id, ..)) = tui.selected_entry()
{ {
match self.copy_entry(id) { if tui.copying_entry == Some(id) {
Ok((new_id, contents, mime)) => { log::debug!(
if new_id != id { "Skipping duplicate copy for entry {id} (already in \
tui.dirty = true; progress)"
} );
let opts = Options::new(); } else {
let mime_type = match mime { tui.copying_entry = Some(id);
Some(ref m) if m == "text/plain" => MimeType::Text, match self.copy_entry(id) {
Some(ref m) => MimeType::Specific(m.clone().to_owned()), Ok((new_id, contents, mime)) => {
None => MimeType::Text, if new_id != id {
}; tui.dirty = true;
let copy_result = opts }
.copy(Source::Bytes(contents.clone().into()), mime_type); let opts = Options::new();
match copy_result { let mime_type = match mime {
Ok(()) => { Some(ref m) if m == "text/plain" => MimeType::Text,
let _ = Notification::new() Some(ref m) => MimeType::Specific(m.clone().to_owned()),
.summary("Stash") None => MimeType::Text,
.body("Copied entry to clipboard") };
.show(); let copy_result = opts
}, .copy(Source::Bytes(contents.clone().into()), mime_type);
Err(e) => { match copy_result {
log::error!("Failed to copy entry to clipboard: {e}"); Ok(()) => {
let _ = Notification::new() let _ = Notification::new()
.summary("Stash") .summary("Stash")
.body(&format!("Failed to copy to clipboard: {e}")) .body("Copied entry to clipboard")
.show(); .show();
}, },
} Err(e) => {
}, log::error!("Failed to copy entry to clipboard: {e}");
Err(e) => { let _ = Notification::new()
log::error!("Failed to fetch entry {id}: {e}"); .summary("Stash")
let _ = Notification::new() .body(&format!("Failed to copy to clipboard: {e}"))
.summary("Stash") .show();
.body(&format!("Failed to fetch entry: {e}")) },
.show(); }
}, },
Err(e) => {
log::error!("Failed to fetch entry {id}: {e}");
let _ = Notification::new()
.summary("Stash")
.body(&format!("Failed to fetch entry: {e}"))
.show();
},
}
tui.copying_entry = None;
} }
} }
} }