Skip to content

Commit 839eb8f

Browse files
authored
Merge pull request #939 from futpib/feat/rofi-shift-delete
Support deletion in clipcat-menu with rofi
2 parents 2c06060 + 22c53bc commit 839eb8f

File tree

5 files changed

+147
-52
lines changed

5 files changed

+147
-52
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,9 @@ menu_length = 30
411411
# Prompt for the menu.
412412
menu_prompt = "Clipcat"
413413
# Extra arguments to pass to `rofi`.
414+
# Rofi supports deleting entries using kb-custom-1 (default: Alt+1).
415+
# To rebind to Shift+Delete:
416+
# extra_arguments = ["-kb-custom-1", "shift+Delete", "-kb-delete-entry", ""]
414417
extra_arguments = ["-mesg", "Please select a clip"]
415418

416419
# Options for "dmenu".

clipcat-menu/src/cli/mod.rs

Lines changed: 53 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use tokio::runtime::Runtime;
1212
use crate::{
1313
config::Config,
1414
error::{self, Error},
15-
finder::{FinderRunner, FinderType},
15+
finder::{FinderRunner, FinderType, MultiSelectionResult, SingleSelectionResult},
1616
shadow,
1717
};
1818

@@ -187,34 +187,49 @@ impl Cli {
187187
}
188188
None => insert_clip(&clips, &finder, &client, &[ClipboardKind::Clipboard]).await?,
189189
Some(Commands::Remove) => {
190-
let selections = finder.multiple_select(&clips).await?;
191-
let ids: Vec<_> = selections.into_iter().map(|(_, clip)| clip.id).collect();
192-
let removed_ids = client.batch_remove(&ids).await?;
193-
for id in removed_ids {
194-
tracing::info!("Removing clip (id: {id:016x})");
190+
let result = finder.multiple_select(&clips).await?;
191+
match result {
192+
MultiSelectionResult::Select(selections)
193+
| MultiSelectionResult::Delete(selections) => {
194+
let ids: Vec<_> = selections.iter().map(|(_, clip)| clip.id).collect();
195+
let removed_ids = client.batch_remove(&ids).await?;
196+
for id in removed_ids {
197+
tracing::info!("Removing clip (id: {id:016x})");
198+
}
199+
}
200+
MultiSelectionResult::Cancel => {
201+
tracing::info!("Nothing is selected");
202+
}
195203
}
196204
}
197205
Some(Commands::Edit { editor }) => {
198-
let selection = finder.single_select(&clips).await?;
199-
if let Some((_index, metadata)) = selection {
200-
let clip = client.get(metadata.id).await?;
201-
if clip.is_utf8_string() {
202-
let editor = ExternalEditor::new(editor);
203-
let new_data = editor
204-
.execute(&clip.as_utf8_string())
205-
.await
206-
.context(error::CallEditorSnafu)?;
207-
let (ok, new_id) =
208-
client.update(clip.id(), new_data.as_bytes(), clip.mime()).await?;
209-
if ok {
210-
tracing::info!("Editing clip (id: {:016x})", new_id);
206+
let result = finder.single_select(&clips).await?;
207+
match result {
208+
SingleSelectionResult::Select(_, metadata) => {
209+
let clip = client.get(metadata.id).await?;
210+
if clip.is_utf8_string() {
211+
let editor = ExternalEditor::new(editor);
212+
let new_data = editor
213+
.execute(&clip.as_utf8_string())
214+
.await
215+
.context(error::CallEditorSnafu)?;
216+
let (ok, new_id) = client
217+
.update(clip.id(), new_data.as_bytes(), clip.mime())
218+
.await?;
219+
if ok {
220+
tracing::info!("Editing clip (id: {:016x})", new_id);
221+
}
222+
let _ok = client.mark(new_id, ClipboardKind::Clipboard).await?;
223+
drop(client);
211224
}
212-
let _ok = client.mark(new_id, ClipboardKind::Clipboard).await?;
213-
drop(client);
214225
}
215-
} else {
216-
tracing::info!("Nothing is selected");
217-
return Ok(());
226+
SingleSelectionResult::Delete(index, clip) => {
227+
tracing::info!("Deleting clip (index: {index}, id: {:016x})", clip.id);
228+
let _removed_ids = client.batch_remove(&[clip.id]).await?;
229+
}
230+
SingleSelectionResult::Cancel => {
231+
tracing::info!("Nothing is selected");
232+
}
218233
}
219234
}
220235
_ => unreachable!(),
@@ -233,14 +248,21 @@ async fn insert_clip(
233248
client: &Client,
234249
clipboard_kinds: &[ClipboardKind],
235250
) -> Result<(), Error> {
236-
let selection = finder.single_select(clips).await?;
237-
if let Some((index, clip)) = selection {
238-
tracing::info!("Inserting clip (index: {index}, id: {:016x})", clip.id);
239-
for &clipboard_kind in clipboard_kinds {
240-
let _ok = client.mark(clip.id, clipboard_kind).await?;
251+
let result = finder.single_select(clips).await?;
252+
match result {
253+
SingleSelectionResult::Select(index, clip) => {
254+
tracing::info!("Inserting clip (index: {index}, id: {:016x})", clip.id);
255+
for &clipboard_kind in clipboard_kinds {
256+
let _ok = client.mark(clip.id, clipboard_kind).await?;
257+
}
258+
}
259+
SingleSelectionResult::Delete(index, clip) => {
260+
tracing::info!("Deleting clip (index: {index}, id: {:016x})", clip.id);
261+
let _removed_ids = client.batch_remove(&[clip.id]).await?;
262+
}
263+
SingleSelectionResult::Cancel => {
264+
tracing::info!("Nothing is selected");
241265
}
242-
} else {
243-
tracing::info!("Nothing is selected");
244266
}
245267

246268
Ok(())

clipcat-menu/src/finder/external/rofi.rs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ use clipcat_base::ClipEntryMetadata;
33
use crate::{
44
config,
55
finder::{
6-
FinderStream, SelectionMode, external::ExternalProgram, finder_stream::ENTRY_SEPARATOR,
6+
FinderResult, FinderStream, SelectionMode, external::ExternalProgram,
7+
finder_stream::ENTRY_SEPARATOR,
78
},
89
};
910

@@ -61,6 +62,15 @@ impl FinderStream for Rofi {
6162
.collect()
6263
}
6364

65+
fn parse_result(&self, data: &[u8], exit_code: Option<i32>) -> FinderResult {
66+
let indices = self.parse_output(data);
67+
match exit_code {
68+
Some(10) if !indices.is_empty() => FinderResult::Delete(indices),
69+
_ if indices.is_empty() => FinderResult::Cancel,
70+
_ => FinderResult::Select(indices),
71+
}
72+
}
73+
6474
fn set_line_length(&mut self, line_length: usize) { self.line_length = line_length }
6575

6676
fn set_menu_length(&mut self, menu_length: usize) { self.menu_length = menu_length; }
@@ -74,7 +84,7 @@ impl FinderStream for Rofi {
7484
mod tests {
7585
use crate::{
7686
config,
77-
finder::{Rofi, SelectionMode, external::ExternalProgram},
87+
finder::{FinderResult, FinderStream, Rofi, SelectionMode, external::ExternalProgram},
7888
};
7989

8090
#[test]
@@ -122,4 +132,21 @@ mod tests {
122132
]
123133
);
124134
}
135+
136+
#[test]
137+
fn test_parse_result() {
138+
let rofi = Rofi::from(config::Rofi::default());
139+
140+
assert_eq!(rofi.parse_result(b"0", Some(0)), FinderResult::Select(vec![0]));
141+
assert_eq!(rofi.parse_result(b"5", None), FinderResult::Select(vec![5]));
142+
assert_eq!(rofi.parse_result(b"1\n2\n3", Some(0)), FinderResult::Select(vec![1, 2, 3]));
143+
144+
assert_eq!(rofi.parse_result(b"", Some(0)), FinderResult::Cancel);
145+
assert_eq!(rofi.parse_result(b"", Some(1)), FinderResult::Cancel);
146+
147+
assert_eq!(rofi.parse_result(b"3", Some(10)), FinderResult::Delete(vec![3]));
148+
assert_eq!(rofi.parse_result(b"0", Some(10)), FinderResult::Delete(vec![0]));
149+
assert_eq!(rofi.parse_result(b"", Some(10)), FinderResult::Cancel);
150+
assert_eq!(rofi.parse_result(b"5\n2\n3", Some(10)), FinderResult::Delete(vec![5, 2, 3]));
151+
}
125152
}

clipcat-menu/src/finder/finder_stream.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use clipcat_base::ClipEntryMetadata;
22

3+
use crate::finder::FinderResult;
4+
35
pub const ENTRY_SEPARATOR: &str = "\n";
46
pub const INDEX_SEPARATOR: char = ':';
57

@@ -27,6 +29,11 @@ pub trait FinderStream: Send + Sync {
2729
.collect()
2830
}
2931

32+
fn parse_result(&self, data: &[u8], _exit_code: Option<i32>) -> FinderResult {
33+
let indices = self.parse_output(data);
34+
if indices.is_empty() { FinderResult::Cancel } else { FinderResult::Select(indices) }
35+
}
36+
3037
fn set_extra_arguments(&mut self, _arguments: &[String]) {}
3138

3239
fn set_line_length(&mut self, _line_length: usize) {}

clipcat-menu/src/finder/mod.rs

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,27 @@ pub enum SelectionMode {
2323
Multiple,
2424
}
2525

26+
#[derive(Clone, Debug, Eq, PartialEq)]
27+
pub enum FinderResult {
28+
Select(Vec<usize>),
29+
Delete(Vec<usize>),
30+
Cancel,
31+
}
32+
33+
#[derive(Clone, Debug)]
34+
pub enum SingleSelectionResult {
35+
Select(usize, ClipEntryMetadata),
36+
Delete(usize, ClipEntryMetadata),
37+
Cancel,
38+
}
39+
40+
#[derive(Clone, Debug)]
41+
pub enum MultiSelectionResult {
42+
Select(Vec<(usize, ClipEntryMetadata)>),
43+
Delete(Vec<(usize, ClipEntryMetadata)>),
44+
Cancel,
45+
}
46+
2647
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
2748
pub enum FinderType {
2849
#[default]
@@ -138,41 +159,58 @@ impl FinderRunner {
138159
pub async fn single_select(
139160
&self,
140161
clips: &[ClipEntryMetadata],
141-
) -> Result<Option<(usize, ClipEntryMetadata)>, FinderError> {
142-
let selected_indices = self.select(clips, SelectionMode::Single).await?;
143-
if let Some(&selected_index) = selected_indices.first() {
144-
let selected_data = &clips[selected_index];
145-
Ok(Some((selected_index, selected_data.clone())))
146-
} else {
147-
Ok(None)
148-
}
162+
) -> Result<SingleSelectionResult, FinderError> {
163+
let result = self.select(clips, SelectionMode::Single).await?;
164+
Ok(match result {
165+
FinderResult::Select(indices) => match indices.first() {
166+
Some(&index) => SingleSelectionResult::Select(index, clips[index].clone()),
167+
None => SingleSelectionResult::Cancel,
168+
},
169+
FinderResult::Delete(indices) => match indices.first() {
170+
Some(&index) => SingleSelectionResult::Delete(index, clips[index].clone()),
171+
None => SingleSelectionResult::Cancel,
172+
},
173+
FinderResult::Cancel => SingleSelectionResult::Cancel,
174+
})
149175
}
150176

151177
pub async fn multiple_select(
152178
&self,
153179
clips: &[ClipEntryMetadata],
154-
) -> Result<Vec<(usize, ClipEntryMetadata)>, FinderError> {
155-
let selected_indices = self.select(clips, SelectionMode::Multiple).await?;
156-
Ok(selected_indices.into_iter().map(|index| (index, clips[index].clone())).collect())
180+
) -> Result<MultiSelectionResult, FinderError> {
181+
let result = self.select(clips, SelectionMode::Multiple).await?;
182+
let to_selections = |indices: Vec<usize>| {
183+
indices.into_iter().map(|index| (index, clips[index].clone())).collect()
184+
};
185+
Ok(match result {
186+
FinderResult::Select(indices) => MultiSelectionResult::Select(to_selections(indices)),
187+
FinderResult::Delete(indices) => MultiSelectionResult::Delete(to_selections(indices)),
188+
FinderResult::Cancel => MultiSelectionResult::Cancel,
189+
})
157190
}
158191

159192
pub async fn select(
160193
&self,
161194
clips: &[ClipEntryMetadata],
162195
selection_mode: SelectionMode,
163-
) -> Result<Vec<usize>, FinderError> {
196+
) -> Result<FinderResult, FinderError> {
164197
if self.external.is_some() {
165198
self.select_externally(clips, selection_mode).await
166199
} else {
167-
BuiltinFinder::new().select(clips, selection_mode).await
200+
let indices = BuiltinFinder::new().select(clips, selection_mode).await?;
201+
if indices.is_empty() {
202+
Ok(FinderResult::Cancel)
203+
} else {
204+
Ok(FinderResult::Select(indices))
205+
}
168206
}
169207
}
170208

171209
async fn select_externally(
172210
&self,
173211
clips: &[ClipEntryMetadata],
174212
selection_mode: SelectionMode,
175-
) -> Result<Vec<usize>, FinderError> {
213+
) -> Result<FinderResult, FinderError> {
176214
if let Some(external) = &self.external {
177215
let input_data = external.generate_input(clips);
178216
let mut child = external
@@ -184,13 +222,11 @@ impl FinderRunner {
184222
}
185223

186224
let output = child.wait_with_output().await.context(error::ReadStdoutSnafu)?;
187-
if output.stdout.is_empty() {
188-
return Ok(Vec::new());
189-
}
225+
let exit_code = output.status.code();
190226

191-
Ok(external.parse_output(output.stdout.as_slice()))
227+
Ok(external.parse_result(output.stdout.as_slice(), exit_code))
192228
} else {
193-
Ok(Vec::new())
229+
Ok(FinderResult::Cancel)
194230
}
195231
}
196232

0 commit comments

Comments
 (0)