ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/cvsroot/CV/bin/cv
Revision: 1.106
Committed: Mon Jul 22 15:28:15 2024 UTC (2 years, 2 months ago) by root
Branch: MAIN
Changes since 1.105: +45 -8 lines
Log Message:
*** empty log message ***

File Contents

# User Rev Content
1 root 1.1 #!/opt/bin/perl
2    
3 root 1.97 BEGIN {
4     # work around libraries (and perl) outputting numbers in weird formats
5     # when they shouldn't. this is somewhat brutal - better suggestions
6     # appreciated.
7     $ENV{LC_NUMERIC} = "C";
8     $ENV{LANG} = delete $ENV{LC_ALL} if exists $ENV{LC_ALL};
9     }
10    
11 root 1.104 use Gtk2; qw(-init -threads-init);
12     BEGIN { Gtk2::Gdk::Threads->enter };
13 root 1.103
14 root 1.98 BEGIN {
15     require Gtk2::CV::Plugin;
16     require "$ENV{HOME}/.cvrc" if -r "$ENV{HOME}/.cvrc";
17     }
18    
19 root 1.80 use common::sense;
20    
21 root 1.37 use Cwd ();
22     use Encode ();
23 root 1.72 use File::Glob ();
24 root 1.79 use Scalar::Util ();
25 root 1.37
26 root 1.12 use Gtk2::Gdk::Keysyms;
27    
28 root 1.48 use Gtk2::CV;
29    
30 root 1.12 use Gtk2::CV::ImageWindow;
31     use Gtk2::CV::Schnauzer;
32    
33 root 1.97 use Carp (); $Carp::MaxArgLen = 256;
34 root 1.82
35 root 1.48 use Gtk2::CV::Plugin::NameCluster;
36     use Gtk2::CV::Plugin::RCluster;
37 root 1.81 use Gtk2::CV::Plugin::PatRenamer;
38 root 1.96 use Gtk2::CV::Plugin::MetaCluster;
39 root 1.16
40 root 1.86 use AnyEvent::Fork::Template;
41    
42 root 1.87 Gtk2::CV::Jobber::set_template $AnyEvent::Fork::Template;
43    
44     # now we can initialize Gtk2 etc.
45     init Gtk2;
46 root 1.77
47 root 1.16 Gtk2::Rc->parse (Gtk2::CV::find_rcfile "gtkrc");
48    
49 root 1.20 use File::Spec;
50    
51 root 1.106 our $mainwin;
52     our $viewer;
53     our $viewer_count;
54     our $schnauzer;
55     our $info;
56     our $help;
57 root 1.12
58 root 1.25 my $schnauzer_idx = 0;
59    
60 root 1.12 sub new_schnauzer {
61 root 1.20 my $s = new Gtk2::CV::Schnauzer;
62 root 1.12
63     $s->signal_connect_after (key_press_event => \&std_keys);
64 root 1.20 $s->signal_connect (activate => sub {
65     my $label = sprintf "%s (%d)",
66 root 1.75 (Glib::filename_display_name +(File::Spec->splitpath ($_[1]))[2]),
67 root 1.20 -s $_[1];
68     $info->set_label ($label);
69 root 1.62 $viewer->load_image ($_[1]) if $viewer; # TODO: error, or chose ANY viewer
70 root 1.20 });
71 root 1.12
72 root 1.40 Gtk2::CV::Plugin->call (new_schnauzer => $s);
73    
74 root 1.53 $s
75 root 1.12 }
76    
77 root 1.79 our %VIEWER; # global viewer container so we can propagate signals
78    
79     $SIG{USR1} = sub {
80     # I assume glib calls us in a safe enough context to create an idle watcher
81     add Glib::Idle sub {
82     $_->reload for values %VIEWER;
83     0
84     };
85     };
86    
87 root 1.62 sub new_viewer {
88     my $self = new Gtk2::CV::ImageWindow;
89    
90 root 1.79 Scalar::Util::weaken ($VIEWER{$self+0} = $self);
91    
92 root 1.62 $viewer_count++;
93    
94     $self->set_title ("CV: Image");
95    
96     $self->signal_connect (key_press_event => sub {
97     $viewer = $_[0];
98    
99     my $key = $_[1]->keyval;
100     my $state = $_[1]->state;
101    
102     if ($state * "control-mask" && $key == $Gtk2::Gdk::Keysyms{c}) {
103     my $viewer = new_viewer ();
104     $viewer->set_image ($_[0]->{image});
105     $viewer->show_all;
106     1
107     } else {
108     &std_keys
109     or $schnauzer->signal_emit (key_press_event => $_[1])
110     }
111     });
112 root 1.63 $self->signal_connect (delete_event => sub { $_[0]->destroy; 0 });
113 root 1.62 $self->signal_connect (destroy => sub {
114 root 1.79 delete $VIEWER{$_[0]+0};
115 root 1.62 $viewer = undef if $viewer == $_[0];
116    
117     main_quit Gtk2 unless --$viewer_count;
118 root 1.63
119     0
120 root 1.62 });
121    
122     $self->signal_connect (button3_press_event => sub {
123     $mainwin->visible
124     ? $mainwin->hide
125     : $mainwin->show_all;
126 root 1.63
127 root 1.62 1
128     });
129    
130     Gtk2::CV::Plugin->call (new_imagewindow => $self);
131    
132     $self
133     }
134    
135 root 1.12 sub std_keys {
136     my $key = $_[1]->keyval;
137     my $state = $_[1]->state;
138    
139 root 1.21 my $ctrl = $state * "control-mask";
140 root 1.12
141     if ($key == $Gtk2::Gdk::Keysyms{q}) {
142 root 1.62 $viewer->destroy;
143 root 1.12 } elsif ($ctrl && $key == $Gtk2::Gdk::Keysyms{v}) {
144 root 1.20 my $w = new Gtk2::Window;
145 root 1.26
146 root 1.66 $w->set_role ("schnauzer");
147 root 1.26 $w->set_title ("CV: Schnauzer");
148 root 1.20 $w->add (my $s = new_schnauzer);
149     $s->set_dir (File::Spec->curdir);
150 root 1.25 $s->set_geometry_hints;
151 root 1.21 $w->show_all;
152 root 1.25
153 root 1.21 } elsif ($ctrl && $key == $Gtk2::Gdk::Keysyms{h}) {
154     unless ($help) {
155 root 1.73 require Gtk2::Ex::PodViewer;
156 root 1.21
157     $help = new Gtk2::Window;
158 root 1.67 $help->set_role ("help");
159 root 1.26 $help->set_title ("CV: Help");
160 root 1.21 $help->set_default_size (500, 300);
161     $help->signal_connect (delete_event => sub { $help->hide; 1 });
162    
163     $help->add (my $sw = new Gtk2::ScrolledWindow);
164 root 1.73 $sw->add (my $h = new Gtk2::Ex::PodViewer);
165 root 1.21
166     #binmode DATA, ":utf8";
167     $h->load_string (do { local $/; <DATA> });
168     }
169    
170     $help->show_all;
171 root 1.83 } elsif (!$state && $Gtk2::Gdk::Keysyms{a} <= $key && $key <= $Gtk2::Gdk::Keysyms{z}) {
172 root 1.82 #
173 root 1.12 } else {
174 root 1.43 return 0;
175 root 1.12 }
176    
177 root 1.43 1
178 root 1.12 }
179    
180 root 1.98 &cvrc_boot if defined &cvrc_boot;
181    
182 root 1.20 {
183 root 1.62 $viewer = new_viewer;
184     $::cur_viewer = $viewer;
185 root 1.40
186 root 1.20 $schnauzer = new_schnauzer;
187    
188     $mainwin = new Gtk2::Window;
189 root 1.66 $mainwin->set_role ("main");
190 root 1.20 $mainwin->set_title ("CV");
191     $mainwin->add (my $vbox = new Gtk2::VBox);
192 root 1.53 $mainwin->signal_connect (delete_event => sub { $mainwin->hide; 1 });
193 root 1.20
194     $vbox->add ($schnauzer);
195 root 1.22 $vbox->pack_end (my $frame = new Gtk2::Frame, 0, 0, 0);
196 root 1.20 $frame->add (my $hbox = new Gtk2::HBox 0, 0);
197 root 1.45 $hbox->pack_start ((new Gtk2::Label "Info: "), 0, 0, 0);
198     $hbox->pack_end (my $labelwindow = new Gtk2::EventBox, 1, 1, 0);
199     $labelwindow->add ($info = new Gtk2::Label);
200     $labelwindow->signal_connect_after (size_request => sub { $_[1]->width (0); 0 });
201     $info->set (selectable => 1, xalign => 0, justify => "left");
202 root 1.25
203     $schnauzer->set_geometry_hints;
204 root 1.20 }
205 root 1.12
206     if (@ARGV) {
207 root 1.68 my $show_first = sub {
208     $schnauzer->show_all;
209 root 1.85
210     # activate first file, but avoid dirs
211     my $entry = $schnauzer->{entry}[0];
212     my $path = "$entry->[0]/$entry->[1]";
213    
214 root 1.89 $schnauzer->handle_key ($Gtk2::Gdk::Keysyms{space}, new Gtk2::Gdk::ModifierType [])
215 root 1.85 unless -d $path;
216    
217 root 1.68 $viewer->show_all;
218     };
219    
220 root 1.102 my $force_sort = $ARGV[0] eq "--sort" ? shift @ARGV : 0;
221    
222 root 1.106 my $clusterview_path = "$ENV{HOME}/.cv-clusterview-data-record";
223    
224     if ($ARGV[0] eq "--clusterview-init") {
225     open my $fh, ">:utf8", $clusterview_path
226     or die "$clusterview_path: $!\n";
227     print $fh "0\n";
228     exit;
229     } elsif ($ARGV[0] =~ "--clusterview-add(?:=(.*))?") {
230     shift @ARGV;
231     my $name = shift @ARGV;
232     open my $fh, ">>:utf8", $clusterview_path
233     or die "$clusterview_path: $!\n";
234     print $fh JSON::XS::encode_json [$1 // scalar @ARGV, $name, \@ARGV], "\n";
235     exit;
236     } elsif ($ARGV[0] eq "--clusterview") {
237     open my $fh, "<:utf8", $clusterview_path
238     or die "$clusterview_path: $!\n";
239     "0\n" eq scalar readline $fh
240     or die "$clusterview_path: invalid version\n";
241    
242     my $groups;
243    
244     while (<$fh>) {
245     push @$groups, JSON::XS::decode_json $_;
246     }
247    
248     require Gtk2::CV::Plugin::NameCluster;
249     my $namecluster = Gtk2::CV::Plugin::NameCluster->new;
250     $namecluster->{analyse_cb} = sub {
251     (my $groups, $groups) = $groups; # sorry
252     $groups
253     };
254    
255     $namecluster->start ($schnauzer);
256     $schnauzer->{namecluster} = $namecluster;
257     $schnauzer->show_all;
258     $viewer->show_all;
259    
260     } elsif (@ARGV == 1 && $ARGV[0] eq "-0r") {
261 root 1.102 $schnauzer->set_paths ([split /\x00/, <STDIN>], !$force_sort, $show_first);
262 root 1.71 } elsif (@ARGV == 1 && -d $ARGV[0]) {
263 root 1.74 $schnauzer->set_dir (shift, $show_first);
264 root 1.71 } else {
265 root 1.72 if ($ARGV[0] eq "-g") {
266     shift @ARGV;
267     @ARGV = map +(File::Glob::bsd_glob $_, File::Glob::GLOB_BRACE | File::Glob::GLOB_QUOTE), @ARGV;
268     }
269 root 1.102 $schnauzer->set_paths ([@ARGV], !$force_sort, $show_first);
270 root 1.71 }
271 root 1.17 } else {
272 root 1.68 $schnauzer->set_dir (File::Spec->curdir, sub {
273     $mainwin->show_all;
274     $viewer->show_all;
275     });
276 root 1.12 }
277    
278 root 1.98 &cvrc_start if defined &cvrc_start;
279    
280 root 1.12 main Gtk2;
281 root 1.21
282 root 1.69 Gtk2::CV::flush_aio;
283    
284 root 1.21 __DATA__
285 root 1.12
286 root 1.58 =encoding utf-8
287    
288 root 1.11 =head1 NAME
289    
290 root 1.47 cv - a fast gtk+ image viewer loosely modeled after XV
291 root 1.11
292     =head1 SYNOPSIS
293    
294 root 1.84 cv
295    
296     cv directory
297    
298     cv path...
299    
300     cv -g <glob expression...>
301    
302     find .. -print0 | cv -0r
303 root 1.11
304 root 1.102 cv --sort ...
305    
306 root 1.47 =head1 FEATURES
307    
308     CV is supposed to work similar to the venerable XV image viewer, just
309     faster. Why faster?
310    
311     =over 4
312    
313     =item * optimized directory scanning algorithm
314    
315 root 1.55 The directory scanning in CV uses some tricks that - on most modern
316 root 1.47 filesystems - makes it possible to detect filetypes faster than stat()'ing
317     every file. This makes CV suitable for directories with lots of files
318     (10000+).
319    
320     This algorithm is quite unprecise - it doesn't make a difference between
321     files, device nodes, symlinks and the like, and filetype detection is done
322     using the file extension only.
323    
324 root 1.55 On the positive side, it is usually many orders of magnitude faster than
325     traditional scanning techniques (good for directories with 10000 or
326     100000+ files).
327    
328 root 1.53 =item * queuing for all time-consuming background tasks
329    
330     All tasks, such as unlinking files or generating thumbnails, that can be
331     done in the background will be done so - no waiting required, even when
332     changing directories.
333    
334 root 1.47 =item * use of asynchronous I/O
335    
336     CV tries to use asynchronous I/O whereever it makes sense, for example
337 root 1.53 while scanning directories, waiting for stat data, unlinking files or
338     generating thumbnails. This usually decreases scanning times for large
339     directories a bit (especially on RAID devices and over NFS) and makes CV
340     much more interactive.
341 root 1.47
342     =item * fast image loading
343    
344     The time span between the user issuing a command and displaying the new
345     image should be as small as possible. CV uses optimized (especially
346     for JPEG) loading functions and sacrifices some quality (e.g no gamma
347 root 1.55 correction, although this might change) to achieve this speed.
348 root 1.47
349     =item * fast thumbnail creation
350    
351 root 1.55 Thumbnail creation uses both CPU and Disk-I/O. CV interleaves both, so
352     on modern CPUs, thumbnailing is usually limited by I/O speed. Thumbnail
353     creation for JPEGs has been specially optimized and can even take
354     advantage of multiple CPUs.
355 root 1.47
356     =item * minimum optical clutter
357    
358     CV has no menus or other user interface elements that take up a lot of
359 root 1.55 screen space (or are useful for beginning users). The schnauzer windows
360     can also be somewhat crowded.
361 root 1.47
362     The point of an image viewer is viewing images, not a nice GUI. This is
363     similar to XV's behaviour.
364    
365     =item * efficient (and hard to learn) user interface
366    
367     CV uses key combinations. A lot. If you are an experienced XV user, you
368     will find most of these keys familiar. If not, CV might be hard to use at
369     first, but will be an efficient tool later.
370    
371 root 1.55 =item * multi-window GUI
372    
373     CV doesn't force you to use a specific layout, instead it relies on your
374     window manager, thus enabling you to chose whatever layout that suits you
375     most.
376    
377 root 1.47 =item * i18n'ed filename handling throughout
378    
379     As long as glib can recognize your filename encoding (either UTF-8 or
380 root 1.55 locale-specific, depending on the setting of G_BROKEN_FILENAMES) and you
381     have the relevant fonts, CV will display your filenames correctly.
382 root 1.47
383     =item * extensible through plug-ins
384    
385     I have weird plug-ins that access remote databases to find a
386     directory. This is not likely to be of any use to other people. Likewise,
387     others might have weird requirements I cannot dream of.
388    
389     =item * filename clustering
390    
391     Among the standard plug-ins is a filename clustering plug-in, that (in
392     case of tens of thousands images in one directory) might be able to
393     cluster similar names together.
394    
395     =back
396    
397 root 1.11 =head1 DESCRIPTION
398    
399     =head2 THE IMAGE WINDOW
400    
401     You can use the following keys in the image window:
402    
403 root 1.50 q quit the program
404     < half the image size
405     > double the image size
406 root 1.101 , shrink the image by ~9% (opposite of .)
407 root 1.50 . enlarge the image by 10%
408     n reset to normal size
409     m maximize to screensize
410     M maximize to screensize, respecting image aspect
411     ctrl-m toggle maxpect-always mode
412 root 1.105 ctrl-shift-m toggle using current image size as max image size
413 root 1.50 u uncrop
414     r set scaling mode to 'nearest' (fastest)
415     s set scaling mode to 'bilinear' (default)
416     t rotate clockwise 90°
417     T rotate counterclockwise°
418 root 1.64 a apply all rotations loslessly to a jpeg file (using exiftran)
419 root 1.90 ctrl-shift-t apply current rotation for future image loads
420 root 1.50 ctrl-v open a new visual schnauzer window for the current dir
421 root 1.63 ctrl-c clone the current image window
422 root 1.50 ctrl-e run an editor ($CV_EDITOR or "gimp") on the current image
423 root 1.57 ctrl-p fire up the print dialog
424 root 1.88 ctrl-shift-p same as ctrl-p, but automatically selects "ok"
425 root 1.50 escape cancel a crop action
426 root 1.11
427 root 1.33 And when playing movies, these additional keys are active:
428    
429 root 1.50 left rewind by 10 seconds
430     right forward by 10 seconds
431     down rewind by 60 seconds
432     up forward by 60 seconds
433     pg_up rewind by 600 seconds
434     pg_down forward by 600 seconds
435     o toggle on-screen display
436     p pause/unpause
437     escape stop playing
438     9 turn volume down
439     0 turn volume up
440 root 1.33
441 root 1.47 Any other keys will be sent to the default schnauzer window, which can be
442     toggled on and off by right-clicking into the image window.
443 root 1.11
444 root 1.47 Left-clicking into the image window will let you crop the image (usually
445     to zoom into large images that CV scales down).
446 root 1.11
447     =head2 THE VISUAL SCHNAUZER
448    
449 root 1.63 Any image-loading action in a schnauzer window acts on the
450     "last-recently-activated" imagewindow, which currently is simply the last
451     image window that received a keypress.
452    
453 root 1.11 You can use the following keys in the schnauzer window:
454    
455 root 1.47 ctrl-space,
456 root 1.50 space move to and display next image
457 root 1.47 ctrl-backspace,
458 root 1.50 backspace move to and display previous image
459 root 1.47 ctrl-return,
460 root 1.50 return display selected picture, or enter directory
461 root 1.11
462 root 1.50 cursor keys move selection
463     page-up move one page up
464     page-down move one page down
465     home move to first file
466     end move to last file
467    
468     ctrl-a select all files
469     ctrl-shift-a select all files currently displayed in the schnauzer window
470     ctrl-d delete selected files WITHOUT ASKING AGAIN
471 root 1.91 ctrl-g force generation of thumbnails for the selected files
472     ctrl-shift-g remove thumbnails for the selected files
473 root 1.50 ctrl-s rescan current direcory or files updates/deletes etc.
474     ctrl-u update selected (or all) icons if neccessary
475 root 1.76 ctrl-- unselected thumbnailed images
476     ctrl-+ keep only thumbnailed images, deselect others
477 root 1.47
478 root 1.56 ^ go to parent directory (caret).
479    
480 root 1.103 currently disabled:
481 root 1.47 0-9,
482 root 1.50 a-z find the first filename beginning with this letter
483 root 1.47
484     Right-clicking into the schnauzer window displays a pop-up menu with
485     additional actions.
486    
487 root 1.60 =head3 SELECTION
488    
489     You can select entries in the Schnauzer in a variety of ways:
490    
491     =over 4
492    
493     =item Keyboard
494    
495     Moving the cursor with the keyboard will first deselect all files and then
496     select the file you moved to.
497    
498 root 1.61 =item Clicking
499 root 1.60
500 root 1.61 Clicking on an entry will select the one you clicked and deselect all
501     others.
502 root 1.60
503 root 1.61 =item Shift-Clicking
504    
505     Shift-clicking will toggle the selection on the entry under the mouse.
506    
507     =item Dragging
508 root 1.60
509     Dragging will select all entries between the one selected when pushing the
510     button and the one selected when releasing the button. If you move above
511     or below the schnauzer area while drag-selecting, the schnauzer will move
512     up/down one row twice per second. In addition, horizontal mouse movement
513     acts as a kind of invisible horizontal scrollbar.
514    
515     =item Hint: double-click works while click-selecting
516    
517     You can double-click any image while click-selecting to display it
518     without stopping the selection process. This will act as if you normally
519     double-clicked the image to display it, and will toggle the selection
520     twice, resulting in no change.
521    
522     =back
523    
524 root 1.47 =head1 FILES
525    
526     When starting, CV runs the F<.cvrc> file in your F<$HOME> directory as if
527     it were a perl script. in that, you will mostly load plug-ins.
528    
529     Example:
530    
531     system "fping -q -t 10 ether"
532     or require "/fs/cv/cvplugin.pl";
533    
534     This will load a plug-in, but only if the machine I<ether> is reachable
535     (supposedly the plug-in is networked in some way :).
536 root 1.11
537 root 1.79 =head1 ENVIRONMENT VARIABLES
538 root 1.27
539     =over 4
540    
541 root 1.38 =item CV_EDITOR
542    
543     The program that gets executed when the user presses C<CTRL-e> in the
544     Schnauzer or image window. The default is C<gimp>.
545    
546 root 1.70 =item CV_AUDIO_PLAYER
547    
548 root 1.94 EXPERIMENTAL: audio playback is now via mpv, this variable is currently
549     ignored.
550    
551 root 1.70 Program used to play all sorts of audio (wav, aif, mp3, ogg...), default "play".
552     Will be called like C<< $CV_AUDIO_PLAYER -- <path> >>.
553    
554 root 1.92 =item CV_MPLAYER
555    
556     Program used to play all sorts of video files. Unlike C<CV_AUDIO_PLAYER>,
557 root 1.99 this really must be some version of the C<mpv> programs, or something that
558     is very command-line compatible to them.
559 root 1.92
560 root 1.93 Note: for video-thumbnailing, mplayer is still used (and hardcoded).
561    
562 root 1.27 =item CV_PRINT_DESTINATION
563    
564     The default (perl-style) destination to use in the print dialog.
565    
566 root 1.38 =item CV_TRASHCAN
567    
568 root 1.78 When set, must point to a directory where all files that are deleted by
569     the "Delete Physically" (ctrl-d) action are moved to (other deletion
570     actions still delete!). If unset, files that are deleted are really being
571     deleted.
572 root 1.38
573 root 1.27 =back
574    
575 root 1.79 =head1 SIGNALS
576    
577     Sending CV a SIGUSR1 signal will cause all image viewers to reload the
578     currently loaded image. This is useful if you use CV as a viewer for
579     changing data - just run it in the background with some path and each time
580     the image changes, send it a SIGUSR1.
581    
582 root 1.23 =head1 SECURITY CONSIDERATIONS
583    
584 root 1.54 CV uses Pixbuf to load non-JPEG images. Pixbuf is not considered safe for
585     this purpose, though (from the gtk-2.2 release notes):
586 root 1.23
587     "While efforts have been made to make gdk-pixbuf robust against invalid
588     images, using gdk-pixbuf to load untrusted data is not recommended, due to
589     the likelyhood that there are additional problems where an invalid image
590     could cause gdk-pixbuf to crash or worse."
591    
592 root 1.11 =head1 BUGS/TODO
593 root 1.23
594 root 1.47 Lots of functionality is missing.
595    
596     Pixbuf doesn't always honor G_BROKEN_FILENAMES, so accessing files with
597 root 1.54 names incompatible with utf-8 might fail.
598 root 1.11
599     rotate on disk
600     lots of ui issues
601     save(?)
602     preferences
603    
604     =head1 AUTHOR
605    
606     Marc Lehmann <cv@plan9.de>.
607    
608     =cut
609 root 1.1