ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/cvsroot/CV/bin/cv
Revision: 1.107
Committed: Tue Jul 23 02:52:31 2024 UTC (2 years, 2 months ago) by root
Branch: MAIN
CVS Tags: HEAD
Changes since 1.106: +1 -0 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 root 1.107 $namecluster->{nosort} = 1;
251 root 1.106 $namecluster->{analyse_cb} = sub {
252     (my $groups, $groups) = $groups; # sorry
253     $groups
254     };
255    
256     $namecluster->start ($schnauzer);
257     $schnauzer->{namecluster} = $namecluster;
258     $schnauzer->show_all;
259     $viewer->show_all;
260    
261     } elsif (@ARGV == 1 && $ARGV[0] eq "-0r") {
262 root 1.102 $schnauzer->set_paths ([split /\x00/, <STDIN>], !$force_sort, $show_first);
263 root 1.71 } elsif (@ARGV == 1 && -d $ARGV[0]) {
264 root 1.74 $schnauzer->set_dir (shift, $show_first);
265 root 1.71 } else {
266 root 1.72 if ($ARGV[0] eq "-g") {
267     shift @ARGV;
268     @ARGV = map +(File::Glob::bsd_glob $_, File::Glob::GLOB_BRACE | File::Glob::GLOB_QUOTE), @ARGV;
269     }
270 root 1.102 $schnauzer->set_paths ([@ARGV], !$force_sort, $show_first);
271 root 1.71 }
272 root 1.17 } else {
273 root 1.68 $schnauzer->set_dir (File::Spec->curdir, sub {
274     $mainwin->show_all;
275     $viewer->show_all;
276     });
277 root 1.12 }
278    
279 root 1.98 &cvrc_start if defined &cvrc_start;
280    
281 root 1.12 main Gtk2;
282 root 1.21
283 root 1.69 Gtk2::CV::flush_aio;
284    
285 root 1.21 __DATA__
286 root 1.12
287 root 1.58 =encoding utf-8
288    
289 root 1.11 =head1 NAME
290    
291 root 1.47 cv - a fast gtk+ image viewer loosely modeled after XV
292 root 1.11
293     =head1 SYNOPSIS
294    
295 root 1.84 cv
296    
297     cv directory
298    
299     cv path...
300    
301     cv -g <glob expression...>
302    
303     find .. -print0 | cv -0r
304 root 1.11
305 root 1.102 cv --sort ...
306    
307 root 1.47 =head1 FEATURES
308    
309     CV is supposed to work similar to the venerable XV image viewer, just
310     faster. Why faster?
311    
312     =over 4
313    
314     =item * optimized directory scanning algorithm
315    
316 root 1.55 The directory scanning in CV uses some tricks that - on most modern
317 root 1.47 filesystems - makes it possible to detect filetypes faster than stat()'ing
318     every file. This makes CV suitable for directories with lots of files
319     (10000+).
320    
321     This algorithm is quite unprecise - it doesn't make a difference between
322     files, device nodes, symlinks and the like, and filetype detection is done
323     using the file extension only.
324    
325 root 1.55 On the positive side, it is usually many orders of magnitude faster than
326     traditional scanning techniques (good for directories with 10000 or
327     100000+ files).
328    
329 root 1.53 =item * queuing for all time-consuming background tasks
330    
331     All tasks, such as unlinking files or generating thumbnails, that can be
332     done in the background will be done so - no waiting required, even when
333     changing directories.
334    
335 root 1.47 =item * use of asynchronous I/O
336    
337     CV tries to use asynchronous I/O whereever it makes sense, for example
338 root 1.53 while scanning directories, waiting for stat data, unlinking files or
339     generating thumbnails. This usually decreases scanning times for large
340     directories a bit (especially on RAID devices and over NFS) and makes CV
341     much more interactive.
342 root 1.47
343     =item * fast image loading
344    
345     The time span between the user issuing a command and displaying the new
346     image should be as small as possible. CV uses optimized (especially
347     for JPEG) loading functions and sacrifices some quality (e.g no gamma
348 root 1.55 correction, although this might change) to achieve this speed.
349 root 1.47
350     =item * fast thumbnail creation
351    
352 root 1.55 Thumbnail creation uses both CPU and Disk-I/O. CV interleaves both, so
353     on modern CPUs, thumbnailing is usually limited by I/O speed. Thumbnail
354     creation for JPEGs has been specially optimized and can even take
355     advantage of multiple CPUs.
356 root 1.47
357     =item * minimum optical clutter
358    
359     CV has no menus or other user interface elements that take up a lot of
360 root 1.55 screen space (or are useful for beginning users). The schnauzer windows
361     can also be somewhat crowded.
362 root 1.47
363     The point of an image viewer is viewing images, not a nice GUI. This is
364     similar to XV's behaviour.
365    
366     =item * efficient (and hard to learn) user interface
367    
368     CV uses key combinations. A lot. If you are an experienced XV user, you
369     will find most of these keys familiar. If not, CV might be hard to use at
370     first, but will be an efficient tool later.
371    
372 root 1.55 =item * multi-window GUI
373    
374     CV doesn't force you to use a specific layout, instead it relies on your
375     window manager, thus enabling you to chose whatever layout that suits you
376     most.
377    
378 root 1.47 =item * i18n'ed filename handling throughout
379    
380     As long as glib can recognize your filename encoding (either UTF-8 or
381 root 1.55 locale-specific, depending on the setting of G_BROKEN_FILENAMES) and you
382     have the relevant fonts, CV will display your filenames correctly.
383 root 1.47
384     =item * extensible through plug-ins
385    
386     I have weird plug-ins that access remote databases to find a
387     directory. This is not likely to be of any use to other people. Likewise,
388     others might have weird requirements I cannot dream of.
389    
390     =item * filename clustering
391    
392     Among the standard plug-ins is a filename clustering plug-in, that (in
393     case of tens of thousands images in one directory) might be able to
394     cluster similar names together.
395    
396     =back
397    
398 root 1.11 =head1 DESCRIPTION
399    
400     =head2 THE IMAGE WINDOW
401    
402     You can use the following keys in the image window:
403    
404 root 1.50 q quit the program
405     < half the image size
406     > double the image size
407 root 1.101 , shrink the image by ~9% (opposite of .)
408 root 1.50 . enlarge the image by 10%
409     n reset to normal size
410     m maximize to screensize
411     M maximize to screensize, respecting image aspect
412     ctrl-m toggle maxpect-always mode
413 root 1.105 ctrl-shift-m toggle using current image size as max image size
414 root 1.50 u uncrop
415     r set scaling mode to 'nearest' (fastest)
416     s set scaling mode to 'bilinear' (default)
417     t rotate clockwise 90°
418     T rotate counterclockwise°
419 root 1.64 a apply all rotations loslessly to a jpeg file (using exiftran)
420 root 1.90 ctrl-shift-t apply current rotation for future image loads
421 root 1.50 ctrl-v open a new visual schnauzer window for the current dir
422 root 1.63 ctrl-c clone the current image window
423 root 1.50 ctrl-e run an editor ($CV_EDITOR or "gimp") on the current image
424 root 1.57 ctrl-p fire up the print dialog
425 root 1.88 ctrl-shift-p same as ctrl-p, but automatically selects "ok"
426 root 1.50 escape cancel a crop action
427 root 1.11
428 root 1.33 And when playing movies, these additional keys are active:
429    
430 root 1.50 left rewind by 10 seconds
431     right forward by 10 seconds
432     down rewind by 60 seconds
433     up forward by 60 seconds
434     pg_up rewind by 600 seconds
435     pg_down forward by 600 seconds
436     o toggle on-screen display
437     p pause/unpause
438     escape stop playing
439     9 turn volume down
440     0 turn volume up
441 root 1.33
442 root 1.47 Any other keys will be sent to the default schnauzer window, which can be
443     toggled on and off by right-clicking into the image window.
444 root 1.11
445 root 1.47 Left-clicking into the image window will let you crop the image (usually
446     to zoom into large images that CV scales down).
447 root 1.11
448     =head2 THE VISUAL SCHNAUZER
449    
450 root 1.63 Any image-loading action in a schnauzer window acts on the
451     "last-recently-activated" imagewindow, which currently is simply the last
452     image window that received a keypress.
453    
454 root 1.11 You can use the following keys in the schnauzer window:
455    
456 root 1.47 ctrl-space,
457 root 1.50 space move to and display next image
458 root 1.47 ctrl-backspace,
459 root 1.50 backspace move to and display previous image
460 root 1.47 ctrl-return,
461 root 1.50 return display selected picture, or enter directory
462 root 1.11
463 root 1.50 cursor keys move selection
464     page-up move one page up
465     page-down move one page down
466     home move to first file
467     end move to last file
468    
469     ctrl-a select all files
470     ctrl-shift-a select all files currently displayed in the schnauzer window
471     ctrl-d delete selected files WITHOUT ASKING AGAIN
472 root 1.91 ctrl-g force generation of thumbnails for the selected files
473     ctrl-shift-g remove thumbnails for the selected files
474 root 1.50 ctrl-s rescan current direcory or files updates/deletes etc.
475     ctrl-u update selected (or all) icons if neccessary
476 root 1.76 ctrl-- unselected thumbnailed images
477     ctrl-+ keep only thumbnailed images, deselect others
478 root 1.47
479 root 1.56 ^ go to parent directory (caret).
480    
481 root 1.103 currently disabled:
482 root 1.47 0-9,
483 root 1.50 a-z find the first filename beginning with this letter
484 root 1.47
485     Right-clicking into the schnauzer window displays a pop-up menu with
486     additional actions.
487    
488 root 1.60 =head3 SELECTION
489    
490     You can select entries in the Schnauzer in a variety of ways:
491    
492     =over 4
493    
494     =item Keyboard
495    
496     Moving the cursor with the keyboard will first deselect all files and then
497     select the file you moved to.
498    
499 root 1.61 =item Clicking
500 root 1.60
501 root 1.61 Clicking on an entry will select the one you clicked and deselect all
502     others.
503 root 1.60
504 root 1.61 =item Shift-Clicking
505    
506     Shift-clicking will toggle the selection on the entry under the mouse.
507    
508     =item Dragging
509 root 1.60
510     Dragging will select all entries between the one selected when pushing the
511     button and the one selected when releasing the button. If you move above
512     or below the schnauzer area while drag-selecting, the schnauzer will move
513     up/down one row twice per second. In addition, horizontal mouse movement
514     acts as a kind of invisible horizontal scrollbar.
515    
516     =item Hint: double-click works while click-selecting
517    
518     You can double-click any image while click-selecting to display it
519     without stopping the selection process. This will act as if you normally
520     double-clicked the image to display it, and will toggle the selection
521     twice, resulting in no change.
522    
523     =back
524    
525 root 1.47 =head1 FILES
526    
527     When starting, CV runs the F<.cvrc> file in your F<$HOME> directory as if
528     it were a perl script. in that, you will mostly load plug-ins.
529    
530     Example:
531    
532     system "fping -q -t 10 ether"
533     or require "/fs/cv/cvplugin.pl";
534    
535     This will load a plug-in, but only if the machine I<ether> is reachable
536     (supposedly the plug-in is networked in some way :).
537 root 1.11
538 root 1.79 =head1 ENVIRONMENT VARIABLES
539 root 1.27
540     =over 4
541    
542 root 1.38 =item CV_EDITOR
543    
544     The program that gets executed when the user presses C<CTRL-e> in the
545     Schnauzer or image window. The default is C<gimp>.
546    
547 root 1.70 =item CV_AUDIO_PLAYER
548    
549 root 1.94 EXPERIMENTAL: audio playback is now via mpv, this variable is currently
550     ignored.
551    
552 root 1.70 Program used to play all sorts of audio (wav, aif, mp3, ogg...), default "play".
553     Will be called like C<< $CV_AUDIO_PLAYER -- <path> >>.
554    
555 root 1.92 =item CV_MPLAYER
556    
557     Program used to play all sorts of video files. Unlike C<CV_AUDIO_PLAYER>,
558 root 1.99 this really must be some version of the C<mpv> programs, or something that
559     is very command-line compatible to them.
560 root 1.92
561 root 1.93 Note: for video-thumbnailing, mplayer is still used (and hardcoded).
562    
563 root 1.27 =item CV_PRINT_DESTINATION
564    
565     The default (perl-style) destination to use in the print dialog.
566    
567 root 1.38 =item CV_TRASHCAN
568    
569 root 1.78 When set, must point to a directory where all files that are deleted by
570     the "Delete Physically" (ctrl-d) action are moved to (other deletion
571     actions still delete!). If unset, files that are deleted are really being
572     deleted.
573 root 1.38
574 root 1.27 =back
575    
576 root 1.79 =head1 SIGNALS
577    
578     Sending CV a SIGUSR1 signal will cause all image viewers to reload the
579     currently loaded image. This is useful if you use CV as a viewer for
580     changing data - just run it in the background with some path and each time
581     the image changes, send it a SIGUSR1.
582    
583 root 1.23 =head1 SECURITY CONSIDERATIONS
584    
585 root 1.54 CV uses Pixbuf to load non-JPEG images. Pixbuf is not considered safe for
586     this purpose, though (from the gtk-2.2 release notes):
587 root 1.23
588     "While efforts have been made to make gdk-pixbuf robust against invalid
589     images, using gdk-pixbuf to load untrusted data is not recommended, due to
590     the likelyhood that there are additional problems where an invalid image
591     could cause gdk-pixbuf to crash or worse."
592    
593 root 1.11 =head1 BUGS/TODO
594 root 1.23
595 root 1.47 Lots of functionality is missing.
596    
597     Pixbuf doesn't always honor G_BROKEN_FILENAMES, so accessing files with
598 root 1.54 names incompatible with utf-8 might fail.
599 root 1.11
600     rotate on disk
601     lots of ui issues
602     save(?)
603     preferences
604    
605     =head1 AUTHOR
606    
607     Marc Lehmann <cv@plan9.de>.
608    
609     =cut
610 root 1.1