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