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