Skip to content

Commit e0ec3bc

Browse files
committed
clipdelmenu: Support deleting multiple entries
dmenu supports selecting multiple entries via ^Enter, so let's extend this. No change for clipmenu. prompt_user_for_hash also seems a bit useless, I don't remember why I extracted that. Let's nuke it given the refactor anyway. Closes: #262
1 parent f381311 commit e0ec3bc

3 files changed

Lines changed: 69 additions & 41 deletions

File tree

man/clipdelmenu.1

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ clipdelmenu \- interactive launcher for deleting clipboard entries
99
provides an interactive interface for browsing and deleting clipboard entries
1010
stored by clipmenud. It launches a menu using a configured launcher
1111
(e.g., dmenu, rofi, or another custom command) and displays a numbered list of
12-
stored clips. Once a selection is made, the chosen clip is permanently deleted
13-
from the clip store.
12+
stored clips. Once a selection is made, the chosen clip(s) are permanently
13+
deleted from the clip store. Multiple clips can be selected at once using the
14+
launcher's multi-select feature (e.g., Ctrl-Enter in dmenu).
1415
.SH OPTIONS
1516
.TP
1617
.B \-h, \--help

src/menu_util.c

Lines changed: 57 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
#include "util.h"
1515

1616
#define MAX_ARGS 32
17+
/* "[N] " prefix + snip line + " (N lines)\n" suffix + NUL */
18+
#define LAUNCHER_LINE_MAX \
19+
(1 + UINT64_MAX_STRLEN + 2 + (CS_SNIP_LINE_SIZE - 1) + 2 + \
20+
UINT64_MAX_STRLEN + 7 + 1 + 1)
1721

1822
static int dmenu_user_argc;
1923
static char **dmenu_user_argv;
@@ -109,13 +113,38 @@ static int wait_for_pid(pid_t pid, int *status) {
109113
return 0;
110114
}
111115

116+
static int parse_sel_idx(const char *line, size_t cur_clips, size_t *out_idx) {
117+
if (line[0] != '[') {
118+
return -1;
119+
}
120+
const char *start = line + 1;
121+
const char *end = strchr(start, ']');
122+
if (!end) {
123+
return -1;
124+
}
125+
char idx_str[UINT64_MAX_STRLEN + 1];
126+
size_t len = (size_t)(end - start);
127+
if (len >= sizeof(idx_str)) {
128+
return -1;
129+
}
130+
memcpy(idx_str, start, len);
131+
idx_str[len] = '\0';
132+
uint64_t sel_idx;
133+
if (str_to_uint64(idx_str, &sel_idx) < 0 || sel_idx == 0 ||
134+
sel_idx > cur_clips) {
135+
return -1;
136+
}
137+
*out_idx = (size_t)(sel_idx - 1);
138+
return 0;
139+
}
140+
112141
/**
113142
* Writes the available clips to the launcher and reads back the user's
114-
* selection.
143+
* selection(s), calling action for each selected clip.
115144
*/
116145
static int _nonnull_ interact_with_dmenu(struct config *cfg, int *input_pipe,
117146
int *output_pipe, pid_t launcher_pid,
118-
uint64_t *out_hash) {
147+
clip_action_fn action) {
119148
close(input_pipe[0]);
120149
close(output_pipe[1]);
121150

@@ -209,26 +238,31 @@ static int _nonnull_ interact_with_dmenu(struct config *cfg, int *input_pipe,
209238
close(input_pipe[1]);
210239
expect(sigaction(SIGPIPE, &old_sa, NULL) == 0);
211240

212-
char sel_idx_str[UINT64_MAX_STRLEN + 1];
213-
read_safe(output_pipe[0], sel_idx_str, 1); // Discard the leading "["
214-
size_t read_sz = read_safe(output_pipe[0], sel_idx_str, UINT64_MAX_STRLEN);
215-
sel_idx_str[read_sz] = '\0';
216-
char *end_ptr = strchr(sel_idx_str, ']');
217-
if (end_ptr) {
218-
*end_ptr = '\0';
219-
}
220-
221-
uint64_t sel_idx;
222-
if (str_to_uint64(sel_idx_str, &sel_idx) < 0 || sel_idx == 0 ||
223-
sel_idx > cur_clips) {
224-
forced_ret = EXIT_FAILURE;
225-
} else {
226-
*out_hash = idx_to_hash[sel_idx - 1];
241+
_drop_(fclose) FILE *output = fdopen(output_pipe[0], "r");
242+
expect(output != NULL);
243+
char line[LAUNCHER_LINE_MAX];
244+
while (!forced_ret && fgets(line, sizeof(line), output) != NULL) {
245+
size_t len = strlen(line);
246+
if (len > 0 && line[len - 1] == '\n') {
247+
line[len - 1] = '\0';
248+
len--;
249+
}
250+
if (len == 0) {
251+
continue;
252+
}
253+
size_t idx;
254+
if (parse_sel_idx(line, cur_clips, &idx) == 0) {
255+
int ret = action(cfg, idx_to_hash[idx]);
256+
if (ret != 0) {
257+
forced_ret = ret;
258+
}
259+
} else {
260+
forced_ret = EXIT_FAILURE;
261+
}
227262
}
228263

229264
int dmenu_status;
230265
int wait_ret = wait_for_pid(launcher_pid, &dmenu_status);
231-
close(output_pipe[0]);
232266

233267
if (forced_ret || wait_ret < 0 || !WIFEXITED(dmenu_status)) {
234268
return EXIT_FAILURE;
@@ -238,11 +272,11 @@ static int _nonnull_ interact_with_dmenu(struct config *cfg, int *input_pipe,
238272
}
239273

240274
/**
241-
* Prompts the user to select a clip via their launcher, and executes
242-
* the provided action on the selected clip.
275+
* Prompts the user to select clip(s) via their configured launcher, and
276+
* executes the provided action on each selected clip.
243277
*/
244-
static int _nonnull_ prompt_user_for_hash(struct config *cfg,
245-
const char *prompt, uint64_t *hash) {
278+
int _nonnull_ menu_prompt_and_act(struct config *cfg, const char *prompt,
279+
clip_action_fn action) {
246280
int input_pipe[2], output_pipe[2];
247281
expect(pipe(input_pipe) == 0 && pipe(output_pipe) == 0);
248282

@@ -253,23 +287,7 @@ static int _nonnull_ prompt_user_for_hash(struct config *cfg,
253287
exec_launcher(cfg, prompt, input_pipe, output_pipe);
254288
}
255289

256-
return interact_with_dmenu(cfg, input_pipe, output_pipe, pid, hash);
257-
}
258-
259-
/**
260-
* Prompts the user to select a clip via their configured launcher, and
261-
* executes the provided action on the selected clip.
262-
*/
263-
int _nonnull_ menu_prompt_and_act(struct config *cfg, const char *prompt,
264-
clip_action_fn action) {
265-
uint64_t hash;
266-
int dmenu_exit_code = prompt_user_for_hash(cfg, prompt, &hash);
267-
268-
if (dmenu_exit_code == EXIT_SUCCESS) {
269-
return action(cfg, hash);
270-
}
271-
272-
return dmenu_exit_code;
290+
return interact_with_dmenu(cfg, input_pipe, output_pipe, pid, action);
273291
}
274292

275293
/**

tests/x_integration_tests

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,15 @@ clipmenu || true
240240
SELECT='[3] test1' clipdelmenu
241241
SELECT='[3] test3' clipdelmenu
242242

243+
# Test multi-select deletion
244+
primary multi1
245+
primary multi2
246+
settle
247+
check_nr_clips 4
248+
249+
SELECT=$'[3] multi1\n[4] multi2' clipdelmenu
250+
check_nr_clips 2
251+
243252
# Check selecting starts serving
244253
xsel -pc
245254

0 commit comments

Comments
 (0)