File Coverage

blib/lib/Clipboard/Any.pm
Criterion Covered Total %
statement 20 242 8.2
branch 0 172 0.0
condition 0 21 0.0
subroutine 7 15 46.6
pod 7 7 100.0
total 34 457 7.4


line stmt bran cond sub pod time code
1             package Clipboard::Any;
2              
3 1     1   514159 use 5.010001;
  1         5  
4 1     1   9 use strict;
  1         2  
  1         34  
5 1     1   5 use warnings;
  1         2  
  1         163  
6 1     1   6438 use Log::ger;
  1         899  
  1         14  
7              
8 1     1   3021 use Exporter::Rinci qw(import);
  1         847  
  1         8  
9 1     1   789 use IPC::System::Options 'system', 'readpipe', 'run', -log=>1;
  1         7201  
  1         8  
10              
11             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
12             our $DATE = '2025-06-16'; # DATE
13             our $DIST = 'Clipboard-Any'; # DIST
14             our $VERSION = '0.015'; # VERSION
15              
16             our $known_clipboard_managers = [qw/klipper parcellite clipit xclip/];
17             our $sch_clipboard_manager = ['str', in=>$known_clipboard_managers];
18             our %argspecopt_clipboard_manager = (
19             clipboard_manager => {
20             summary => 'Explicitly set clipboard manager to use',
21             schema => $sch_clipboard_manager,
22             description => <<'MARKDOWN',
23              
24             The default, when left undef, is to detect what clipboard manager is running.
25              
26             MARKDOWN
27             cmdline_aliases => {m=>{}},
28             },
29             );
30              
31             our %argspec0_index = (
32             index => {
33             summary => 'Index of item in history (0 means the current/latest, 1 the second latest, and so on)',
34             schema => 'int*',
35             description => <<'MARKDOWN',
36              
37             If the index exceeds the number of items in history, empty string or undef will
38             be returned instead.
39              
40             MARKDOWN
41             },
42             );
43              
44             our %SPEC;
45              
46             sub _find_qdbus {
47 0     0     require File::Which;
48              
49 0           my @paths;
50 0 0         if (my $path = File::Which::which("qdbus")) {
51 0           log_trace "qdbus found in PATH: $path";
52 0           push @paths, $path;
53             } else {
54 0           for my $dir ("/usr/lib/qt6/bin", "/usr/lib/qt5/bin") {
55 0 0 0       if ((-d $dir) && (-x "$dir/qdbus")) {
56 0           log_trace "qdbus found in $dir";
57 0           push @paths, "$dir/qdbus";
58             }
59             }
60             }
61              
62 0           @paths;
63             }
64              
65             $SPEC{':package'} = {
66             v => 1.1,
67             summary => 'Common interface to clipboard manager functions',
68             description => <<'MARKDOWN',
69              
70             This module provides common functions related to clipboard manager.
71              
72             Supported clipboard manager: KDE Plasma's Klipper (`klipper`), `parcellite`,
73             `clipit`, `xclip`. Support for more clipboard managers, e.g. on Windows or other
74             Linux desktop environment is welcome.
75              
76             MARKDOWN
77             };
78              
79             $SPEC{'detect_clipboard_manager'} = {
80             v => 1.1,
81             summary => 'Detect which clipboard manager program is currently running',
82             description => <<'MARKDOWN',
83              
84             Will return a string containing name of clipboard manager program, e.g.
85             `klipper`. Will return undef if no known clipboard manager is detected.
86              
87             MARKDOWN
88             result_naked => 1,
89             args => {
90             detail => {
91             schema => 'bool*',
92             cmdline_aliases => {l=>{}},
93             },
94             },
95             #result => {
96             # schema => $sch_clipboard_manager,
97             #},
98             };
99             sub detect_clipboard_manager {
100 0     0 1   my %args = @_;
101              
102 0           require File::Which;
103              
104 0           require Proc::Find;
105 1     1   556 no warnings 'once';
  1         3  
  1         3977  
106 0           local $Proc::Find::CACHE = 1;
107              
108 0           my $info = {};
109             DETECT: {
110              
111 0           DETECT_KLIPPER:
112             {
113 0           log_trace "Checking whether clipboard manager klipper is running ...";
  0            
114              
115             METHOD1: {
116 0           my @paths = _find_qdbus();
  0            
117              
118 0 0         unless (@paths) {
119 0           log_trace "qdbus not found, checking using qdbus";
120 0           last;
121             }
122              
123 0           for my $path (@paths) {
124 0           my $out;
125 0           system({capture_merged=>\$out}, $path, "org.kde.klipper", "/klipper");
126 0 0         unless ($? == 0) {
127             # note, when klipper is disabled via System Tray Settings >
128             # General > Extra Items, the object path /klipper disappears.
129 0           log_trace "Failed listing org.kde.klipper /klipper methods (using qdus at $path)";
130 0           next;
131             }
132 0           log_trace "org.kde.klipper/klipper object active, concluding using klipper";
133 0           $info->{manager} = "klipper";
134 0           $info->{klipper_path} = $path;
135 0           last DETECT;
136             }
137             }
138              
139             # we need qdbus anyway
140             #METHOD2: {
141             # my $pids = Proc::Find::find_proc(name => "dbus-daemon");
142             # if (@$pids) {
143             # log_trace "There is dbus-daemon running, assuming we are using klipper";
144             # $info->{manager} = "klipper";
145             # last DETECT;
146             # } else {
147             # log_trace "dbus-daemon process does not seem to be running, probably not using klipper";
148             # }
149             #}
150             } # DETECT_KLIPPER
151              
152             DETECT_PARCELLITE:
153             {
154 0           log_trace "Checking whether clipboard manager parcellite is running ...";
  0            
155 0           my $pids = Proc::Find::find_proc(name => "parcellite");
156 0 0         if (@$pids) {
157 0           log_trace "parcellite process is running, concluding using parcellite";
158 0           $info->{manager} = "parcellite";
159 0           last DETECT;
160             } else {
161 0           log_trace "parcellite process does not seem to be running, probably not using parcellite";
162             }
163             } # DETECT_PARCELLITE
164              
165             DETECT_CLIPIT:
166             {
167             # basically the same as parcellite
168 0           log_trace "Checking whether clipboard manager clipit is running ...";
  0            
169 0           my $pids = Proc::Find::find_proc(name => "clipit");
170 0 0         if (@$pids) {
171 0           log_trace "clipit process is running, concluding using clipit";
172 0           $info->{manager} = "parcellite";
173 0           last DETECT;
174             } else {
175 0           log_trace "clipit process does not seem to be running, probably not using clipit";
176             }
177             } # DETECT_CLIPIT
178              
179             DETECT_XCLIP:
180             {
181 0           log_trace "Checking whether xclip is available ...";
  0            
182 0           my $path = File::Which::which("xclip");
183 0 0         unless ($path) {
184 0           log_trace "xclip not found in PATH, skipping choosing xclip";
185 0           last;
186             }
187 0           log_trace "xclip found in PATH, concluding using xclip";
188 0           $info->{manager} = "xclip";
189 0           $info->{xclip_path} = $path;
190             } # DETECT_XCLIP
191              
192 0           log_trace "No known clipboard manager is detected";
193             } # DETECT
194              
195 0 0         if ($args{detail}) {
196 0           $info;
197             } else {
198 0           $info->{manager};
199             }
200             }
201              
202             $SPEC{'clear_clipboard_history'} = {
203             v => 1.1,
204             summary => 'Delete all clipboard items',
205             description => <<'MARKDOWN',
206              
207             MARKDOWN
208             args => {
209             %argspecopt_clipboard_manager,
210             },
211             };
212             sub clear_clipboard_history {
213 0     0 1   my %args = @_;
214              
215 0   0       my $clipboard_manager = $args{clipboard_manager} // detect_clipboard_manager();
216 0 0         return [412, "Can't detect any known clipboard manager"]
217             unless $clipboard_manager;
218              
219 0 0         if ($clipboard_manager eq 'klipper') {
    0          
    0          
    0          
220 0           my @paths = _find_qdbus();
221 0 0         die "Can't find qdbus" unless @paths;
222 0           my ($stdout, $stderr);
223             # qdbus likes to emit an empty line
224 0           system({capture_stdout=>\$stdout, capture_stderr=>\$stderr},
225             $paths[0], "org.kde.klipper", "/klipper", "clearClipboardHistory");
226 0 0         my $exit_code = $? < 0 ? $? : $?>>8;
227 0 0         return [500, "/klipper's clearClipboardHistory failed: $exit_code"] if $exit_code;
228 0           return [200, "OK"];
229             } elsif ($clipboard_manager eq 'parcellite') {
230 0           return [501, "Not yet implemented"];
231             } elsif ($clipboard_manager eq 'clipit') {
232 0           return [501, "Not yet implemented"];
233             } elsif ($clipboard_manager eq 'xclip') {
234             # implemented by setting both primary and clipboard to empty string
235              
236 0           my $fh;
237              
238 0 0         open $fh, "| xclip -i -selection primary" ## no critic: InputOutput::ProhibitTwoArgOpen
239             or return [500, "xclip -i -selection primary failed (1): $!"];
240 0           print $fh '';
241 0 0         close $fh
242             or return [500, "xclip -i -selection primary failed (2): $!"];
243              
244 0 0         open $fh, "| xclip -i -selection clipboard" ## no critic: InputOutput::ProhibitTwoArgOpen
245             or return [500, "xclip -i -selection clipboard failed (1): $!"];
246 0           print $fh '';
247 0 0         close $fh
248             or return [500, "xclip -i -selection clipboard failed (2): $!"];
249              
250 0           return [200, "OK"];
251             }
252              
253 0           [412, "Cannot clear clipboard history (clipboard manager=$clipboard_manager)"];
254             }
255              
256             $SPEC{'clear_clipboard_content'} = {
257             v => 1.1,
258             summary => 'Delete current clipboard content',
259             description => <<'MARKDOWN',
260              
261             MARKDOWN
262             args => {
263             %argspecopt_clipboard_manager,
264             },
265             };
266             sub clear_clipboard_content {
267 0     0 1   my %args = @_;
268              
269 0   0       my $clipboard_manager = $args{clipboard_manager} // detect_clipboard_manager();
270 0 0         return [412, "Can't detect any known clipboard manager"]
271             unless $clipboard_manager;
272              
273 0 0         if ($clipboard_manager eq 'klipper') {
    0          
    0          
    0          
274 0           my @paths = _find_qdbus();
275 0 0         die "Can't find qdbus" unless @paths;
276 0           my ($stdout, $stderr);
277             # qdbus likes to emit an empty line
278 0           system({capture_stdout=>\$stdout, capture_stderr=>\$stderr},
279             $paths[0], "org.kde.klipper", "/klipper", "clearClipboardContents");
280 0 0         my $exit_code = $? < 0 ? $? : $?>>8;
281 0 0         return [500, "/klipper's clearClipboardContents failed: $exit_code"] if $exit_code;
282 0           return [200, "OK"];
283             } elsif ($clipboard_manager eq 'parcellite') {
284 0           return [501, "Not yet implemented"];
285             } elsif ($clipboard_manager eq 'clipit') {
286 0           return [501, "Not yet implemented"];
287             } elsif ($clipboard_manager eq 'xclip') {
288             # implemented by setting primary to empty string
289              
290 0 0         open my $fh, "| xclip -i -selection primary" ## no critic: InputOutput::ProhibitTwoArgOpen
291             or return [500, "xclip -i -selection primary failed (1): $!"];
292 0           print $fh '';
293 0 0         close $fh
294             or return [500, "xclip -i -selection primary failed (2): $!"];
295              
296 0           return [200, "OK"];
297             }
298              
299 0           [412, "Cannot clear clipboard content (clipboard manager=$clipboard_manager)"];
300             }
301              
302             $SPEC{'get_clipboard_content'} = {
303             v => 1.1,
304             summary => 'Get the clipboard content (most recent, history index [0])',
305             description => <<'MARKDOWN',
306              
307             Caveats for klipper: Non-text item is not retrievable by getClipboardContents().
308             If the current item is e.g. an image, then the next text item from history will
309             be returned instead, or empty string if none exists.
310              
311             MARKDOWN
312             args => {
313             %argspecopt_clipboard_manager,
314             },
315             examples => [
316             {
317             summary => 'Munge text (remove duplicate spaces) in clipboard',
318             src_plang => 'bash',
319             src => q{[[prog]] | perl -lpe's/ {2,}/ /g' | clipadd},
320             test => 0,
321             'x.doc.show_result' => 0,
322             },
323             ],
324             };
325             sub get_clipboard_content {
326 0     0 1   my %args = @_;
327              
328 0   0       my $clipboard_manager = $args{clipboard_manager} // detect_clipboard_manager();
329 0 0         return [412, "Can't detect any known clipboard manager"]
330             unless $clipboard_manager;
331              
332 0 0         if ($clipboard_manager eq 'klipper') {
    0          
    0          
    0          
333 0           my @paths = _find_qdbus();
334 0 0         die "Can't find qdbus" unless @paths;
335 0           my ($stdout, $stderr);
336 0           system({capture_stdout=>\$stdout, capture_stderr=>\$stderr},
337             $paths[0], "org.kde.klipper", "/klipper", "getClipboardContents");
338 0 0         my $exit_code = $? < 0 ? $? : $?>>8;
339 0 0         return [500, "/klipper's getClipboardContents failed: $exit_code"] if $exit_code;
340 0           chomp $stdout;
341 0           return [200, "OK", $stdout];
342             } elsif ($clipboard_manager eq 'parcellite') {
343 0           my ($stdout, $stderr);
344 0           system({capture_stdout=>\$stdout, capture_stderr=>\$stderr},
345             "parcellite", "-p");
346 0 0         my $exit_code = $? < 0 ? $? : $?>>8;
347 0 0         return [500, "parcellite command failed with exit code $exit_code"] if $exit_code;
348 0           return [200, "OK", $stdout];
349             } elsif ($clipboard_manager eq 'clipit') {
350 0           my ($stdout, $stderr);
351 0           system({capture_stdout=>\$stdout, capture_stderr=>\$stderr},
352             "clipit", "-p");
353 0 0         my $exit_code = $? < 0 ? $? : $?>>8;
354 0 0         return [500, "clipit command failed with exit code $exit_code"] if $exit_code;
355 0           return [200, "OK", $stdout];
356             } elsif ($clipboard_manager eq 'xclip') {
357 0           my ($stdout, $stderr);
358 0           system({capture_stdout=>\$stdout, capture_stderr=>\$stderr},
359             "xclip", "-o", "-selection", "primary");
360 0 0         my $exit_code = $? < 0 ? $? : $?>>8;
361 0 0         return [500, "xclip -o failed with exit code $exit_code"] if $exit_code;
362 0           return [200, "OK", $stdout];
363             }
364              
365 0           [412, "Cannot get clipboard content (clipboard manager=$clipboard_manager)"];
366             }
367              
368             $SPEC{'list_clipboard_history'} = {
369             v => 1.1,
370             summary => 'List the clipboard history',
371             description => <<'MARKDOWN',
372              
373             Caveats for klipper: 1) Klipper does not provide method to get the length of
374             history. So we retrieve history item one by one using getClipboardHistoryItem(i)
375             from i=0, i=1, and so on. And assume that if we get two consecutive empty
376             string, it means we reach the end of the clipboard history before the first
377             empty result.
378              
379             2) Non-text items are not retrievable by getClipboardHistoryItem().
380              
381             MARKDOWN
382             args => {
383             %argspecopt_clipboard_manager,
384             },
385             };
386             sub list_clipboard_history {
387 0     0 1   my %args = @_;
388              
389 0   0       my $clipboard_manager = $args{clipboard_manager} // detect_clipboard_manager();
390 0 0         return [412, "Can't detect any known clipboard manager"]
391             unless $clipboard_manager;
392              
393 0 0         if ($clipboard_manager eq 'klipper') {
    0          
    0          
    0          
394 0           my @paths = _find_qdbus();
395 0 0         die "Can't find qdbus" unless @paths;
396 0           my @rows;
397 0           my $i = 0;
398 0           my $got_empty;
399 0           while (1) {
400 0           my ($stdout, $stderr);
401 0           system({capture_stdout=>\$stdout, capture_stderr=>\$stderr},
402             $paths[0], "org.kde.klipper", "/klipper", "getClipboardHistoryItem", $i);
403 0 0         my $exit_code = $? < 0 ? $? : $?>>8;
404 0 0         return [500, "/klipper's getClipboardHistoryItem($i) failed: $exit_code"] if $exit_code;
405 0           chomp $stdout;
406 0 0         if ($stdout eq '') {
407 0           log_trace "Got empty result";
408 0 0         if ($got_empty++) {
409 0           pop @rows;
410 0           last;
411             } else {
412 0           push @rows, $stdout;
413             }
414             } else {
415 0           log_trace "Got result '%s'", $stdout;
416 0           $got_empty = 0;
417 0           push @rows, $stdout;
418             }
419 0           $i++;
420             }
421 0           return [200, "OK", \@rows];
422             } elsif ($clipboard_manager eq 'parcellite') {
423             # parcellite -c usually just prints the same result as -p (primary)
424 0           return [501, "Not yet implemented"];
425             } elsif ($clipboard_manager eq 'clipit') {
426             # clipit -c usually just prints the same result as -p (primary)
427 0           return [501, "Not yet implemented"];
428             } elsif ($clipboard_manager eq 'xclip') {
429 0           my ($stdout, $stderr, $exit_code);
430 0           my @rows;
431              
432 0           system({capture_stdout=>\$stdout, capture_stderr=>\$stderr},
433             "xclip", "-o", "-selection", "primary");
434 0 0         $exit_code = $? < 0 ? $? : $?>>8;
435 0 0         return [500, "xclip -o (primary) failed with exit code $exit_code"] if $exit_code;
436 0           push @rows, $stdout;
437              
438 0           system({capture_stdout=>\$stdout, capture_stderr=>\$stderr},
439             "xclip", "-o", "-selection", "clipboard");
440 0 0         $exit_code = $? < 0 ? $? : $?>>8;
441 0 0         return [500, "xclip -o (clipboard) failed with exit code $exit_code"] if $exit_code;
442 0           push @rows, $stdout;
443              
444 0           return [200, "OK", \@rows];
445             }
446              
447 0           [412, "Cannot list clipboard history (clipboard manager=$clipboard_manager)"];
448             }
449              
450             $SPEC{'get_clipboard_history_item'} = {
451             v => 1.1,
452             summary => 'Get a clipboard history item',
453             description => <<'MARKDOWN',
454              
455             MARKDOWN
456             args => {
457             %argspecopt_clipboard_manager,
458             %argspec0_index,
459             },
460             };
461             sub get_clipboard_history_item {
462 0     0 1   my %args = @_;
463 0           my $index = $args{index};
464              
465 0   0       my $clipboard_manager = $args{clipboard_manager} // detect_clipboard_manager();
466 0 0         return [412, "Can't detect any known clipboard manager"]
467             unless $clipboard_manager;
468              
469 0 0         if ($clipboard_manager eq 'klipper') {
    0          
    0          
    0          
470 0           my @paths = _find_qdbus();
471 0 0         die "Can't find qdbus" unless @paths;
472 0           my ($stdout, $stderr);
473 0           system({capture_stdout=>\$stdout, capture_stderr=>\$stderr},
474             $paths[0], "org.kde.klipper", "/klipper", "getClipboardHistoryItem", $index);
475 0 0         my $exit_code = $? < 0 ? $? : $?>>8;
476 0 0         return [500, "/klipper's getClipboardHistoryItem($index) failed: $exit_code"] if $exit_code;
477 0           chomp $stdout;
478 0           return [200, "OK", $stdout];
479             } elsif ($clipboard_manager eq 'parcellite') {
480             # parcellite -c usually just prints the same result as -p (primary)
481 0           return [501, "Not yet implemented"];
482             } elsif ($clipboard_manager eq 'clipit') {
483             # clipit -c usually just prints the same result as -p (primary)
484 0           return [501, "Not yet implemented"];
485             } elsif ($clipboard_manager eq 'xclip') {
486 0           my ($stdout, $stderr, $exit_code);
487 0           my @rows;
488              
489 0 0         if ($index == 0) {
    0          
490 0           system({capture_stdout=>\$stdout, capture_stderr=>\$stderr},
491             "xclip", "-o", "-selection", "primary");
492 0 0         $exit_code = $? < 0 ? $? : $?>>8;
493 0 0         return [500, "xclip -o (primary) failed with exit code $exit_code"] if $exit_code;
494 0           return [200, "OK", $stdout];
495             } elsif ($index == 0) {
496 0           system({capture_stdout=>\$stdout, capture_stderr=>\$stderr},
497             "xclip", "-o", "-selection", "clipboard");
498 0 0         $exit_code = $? < 0 ? $? : $?>>8;
499 0 0         return [500, "xclip -o (clipboard) failed with exit code $exit_code"] if $exit_code;
500 0           return [200, "OK", $stdout];
501             } else {
502 0           return [200, "OK", undef];
503             }
504             }
505              
506 0           [412, "Cannot get clipboard history item (clipboard manager=$clipboard_manager)"];
507             }
508              
509             $SPEC{'add_clipboard_content'} = {
510             v => 1.1,
511             summary => 'Add a new content to the clipboard',
512             description => <<'MARKDOWN',
513              
514             For `xclip`: when adding content, the primary selection is set. The clipboard
515             content is unchanged.
516              
517             MARKDOWN
518             args => {
519             %argspecopt_clipboard_manager,
520             content => {
521             schema => 'str*',
522             pos=>0,
523             cmdline_src=>'stdin_or_args',
524             },
525             tee => {
526             summary => 'If set to true, will output content back to STDOUT',
527             schema => 'bool*',
528             cmdline_aliases => {t=>{}},
529             },
530             chomp_newline => {
531             summary => 'Remove trailing newlines before adding item to clipboard',
532             schema => 'bool*',
533             cmdline_aliases => {l=>{}},
534             },
535             },
536             examples => [
537             {
538             summary => 'Munge text (remove duplicate spaces) in clipboard',
539             src_plang => 'bash',
540             src => q{clipget | perl -lpe's/ {2,}/ /g' | [[prog]]},
541             test => 0,
542             'x.doc.show_result' => 0,
543             },
544             ],
545             };
546             sub add_clipboard_content {
547 0     0 1   my %args = @_;
548              
549 0   0       my $clipboard_manager = $args{clipboard_manager} // detect_clipboard_manager();
550 0 0         return [412, "Can't detect any known clipboard manager"]
551             unless $clipboard_manager;
552              
553             defined $args{content} or
554 0 0         return [400, "Please specify content"];
555              
556 0           my $content0 = $args{content};
557 0           my $content = $content0;
558 0 0         $content =~ s/\R+\z// if $args{chomp_newline};
559              
560 0 0         if ($clipboard_manager eq 'klipper') {
    0          
    0          
    0          
561 0           my @paths = _find_qdbus();
562 0 0         die "Can't find qdbus" unless @paths;
563 0           my ($stdout, $stderr);
564             # qdbus likes to emit an empty line
565 0           system({capture_stdout=>\$stdout, capture_stderr=>\$stderr},
566             $paths[0], "org.kde.klipper", "/klipper", "setClipboardContents", $content);
567 0 0         my $exit_code = $? < 0 ? $? : $?>>8;
568 0 0         return [500, "/klipper's setClipboardContents failed: $exit_code"] if $exit_code;
569 0 0         print $content0 if $args{tee};
570 0           return [200, "OK"];
571             } elsif ($clipboard_manager eq 'parcellite') {
572             # parcellite cli copies unknown options and stdin to clipboard history
573             # but not as the current one
574 0           return [501, "Not yet implemented"];
575             } elsif ($clipboard_manager eq 'clipit') {
576             # clipit cli copies unknown options and stdin to clipboard history but
577             # not as the current one
578 0           return [501, "Not yet implemented"];
579             } elsif ($clipboard_manager eq 'xclip') {
580 0 0         open my $fh, "| xclip -i -selection primary" ## no critic: InputOutput::ProhibitTwoArgOpen
581             or return [500, "xclip -i -selection primary failed (1): $!"];
582 0           print $fh $content;
583 0 0         close $fh
584             or return [500, "xclip -i -selection primary failed (2): $!"];
585 0 0         print $content0 if $args{tee};
586 0           return [200, "OK"];
587             }
588              
589 0           [412, "Cannot add clipboard content (clipboard manager=$clipboard_manager)"];
590             }
591              
592             1;
593             # ABSTRACT: Common interface to clipboard manager functions
594              
595             __END__