ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/CV/bin/cv
Revision: 1.100
Committed: Thu May 21 20:14:16 2020 UTC (4 years ago) by root
Branch: MAIN
Changes since 1.99: +1 -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.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.71 if (@ARGV == 1 && $ARGV[0] eq "-0r") {
219     local $/;
220 root 1.74 $schnauzer->set_paths ([split /\x00/, <STDIN>], 1, $show_first);
221 root 1.71 } elsif (@ARGV == 1 && -d $ARGV[0]) {
222 root 1.74 $schnauzer->set_dir (shift, $show_first);
223 root 1.71 } else {
224 root 1.72 if ($ARGV[0] eq "-g") {
225     shift @ARGV;
226     @ARGV = map +(File::Glob::bsd_glob $_, File::Glob::GLOB_BRACE | File::Glob::GLOB_QUOTE), @ARGV;
227     }
228 root 1.74 $schnauzer->set_paths ([@ARGV], 1, $show_first);
229 root 1.71 }
230 root 1.17 } else {
231 root 1.68 $schnauzer->set_dir (File::Spec->curdir, sub {
232     $mainwin->show_all;
233     $viewer->show_all;
234     });
235 root 1.12 }
236    
237 root 1.98 &cvrc_start if defined &cvrc_start;
238    
239 root 1.12 main Gtk2;
240 root 1.21
241 root 1.69 Gtk2::CV::flush_aio;
242    
243 root 1.21 __DATA__
244 root 1.12
245 root 1.58 =encoding utf-8
246    
247 root 1.11 =head1 NAME
248    
249 root 1.47 cv - a fast gtk+ image viewer loosely modeled after XV
250 root 1.11
251     =head1 SYNOPSIS
252    
253 root 1.84 cv
254    
255     cv directory
256    
257     cv path...
258    
259     cv -g <glob expression...>
260    
261     find .. -print0 | cv -0r
262 root 1.11
263 root 1.47 =head1 FEATURES
264    
265     CV is supposed to work similar to the venerable XV image viewer, just
266     faster. Why faster?
267    
268     =over 4
269    
270     =item * optimized directory scanning algorithm
271    
272 root 1.55 The directory scanning in CV uses some tricks that - on most modern
273 root 1.47 filesystems - makes it possible to detect filetypes faster than stat()'ing
274     every file. This makes CV suitable for directories with lots of files
275     (10000+).
276    
277     This algorithm is quite unprecise - it doesn't make a difference between
278     files, device nodes, symlinks and the like, and filetype detection is done
279     using the file extension only.
280    
281 root 1.55 On the positive side, it is usually many orders of magnitude faster than
282     traditional scanning techniques (good for directories with 10000 or
283     100000+ files).
284    
285 root 1.53 =item * queuing for all time-consuming background tasks
286    
287     All tasks, such as unlinking files or generating thumbnails, that can be
288     done in the background will be done so - no waiting required, even when
289     changing directories.
290    
291 root 1.47 =item * use of asynchronous I/O
292    
293     CV tries to use asynchronous I/O whereever it makes sense, for example
294 root 1.53 while scanning directories, waiting for stat data, unlinking files or
295     generating thumbnails. This usually decreases scanning times for large
296     directories a bit (especially on RAID devices and over NFS) and makes CV
297     much more interactive.
298 root 1.47
299     =item * fast image loading
300    
301     The time span between the user issuing a command and displaying the new
302     image should be as small as possible. CV uses optimized (especially
303     for JPEG) loading functions and sacrifices some quality (e.g no gamma
304 root 1.55 correction, although this might change) to achieve this speed.
305 root 1.47
306     =item * fast thumbnail creation
307    
308 root 1.55 Thumbnail creation uses both CPU and Disk-I/O. CV interleaves both, so
309     on modern CPUs, thumbnailing is usually limited by I/O speed. Thumbnail
310     creation for JPEGs has been specially optimized and can even take
311     advantage of multiple CPUs.
312 root 1.47
313     =item * minimum optical clutter
314    
315     CV has no menus or other user interface elements that take up a lot of
316 root 1.55 screen space (or are useful for beginning users). The schnauzer windows
317     can also be somewhat crowded.
318 root 1.47
319     The point of an image viewer is viewing images, not a nice GUI. This is
320     similar to XV's behaviour.
321    
322     =item * efficient (and hard to learn) user interface
323    
324     CV uses key combinations. A lot. If you are an experienced XV user, you
325     will find most of these keys familiar. If not, CV might be hard to use at
326     first, but will be an efficient tool later.
327    
328 root 1.55 =item * multi-window GUI
329    
330     CV doesn't force you to use a specific layout, instead it relies on your
331     window manager, thus enabling you to chose whatever layout that suits you
332     most.
333    
334 root 1.47 =item * i18n'ed filename handling throughout
335    
336     As long as glib can recognize your filename encoding (either UTF-8 or
337 root 1.55 locale-specific, depending on the setting of G_BROKEN_FILENAMES) and you
338     have the relevant fonts, CV will display your filenames correctly.
339 root 1.47
340     =item * extensible through plug-ins
341    
342     I have weird plug-ins that access remote databases to find a
343     directory. This is not likely to be of any use to other people. Likewise,
344     others might have weird requirements I cannot dream of.
345    
346     =item * filename clustering
347    
348     Among the standard plug-ins is a filename clustering plug-in, that (in
349     case of tens of thousands images in one directory) might be able to
350     cluster similar names together.
351    
352     =back
353    
354 root 1.11 =head1 DESCRIPTION
355    
356     =head2 THE IMAGE WINDOW
357    
358     You can use the following keys in the image window:
359    
360 root 1.50 q quit the program
361     < half the image size
362     > double the image size
363     , shrink the image by 10%
364     . enlarge the image by 10%
365     n reset to normal size
366     m maximize to screensize
367     M maximize to screensize, respecting image aspect
368     ctrl-m toggle maxpect-always mode
369 root 1.59 ctrl-sift-m toggle using current image size as max image size
370 root 1.50 u uncrop
371     r set scaling mode to 'nearest' (fastest)
372     s set scaling mode to 'bilinear' (default)
373     shift-s set scaling mode to 'hyper' (slowest)
374     t rotate clockwise 90°
375     T rotate counterclockwise°
376 root 1.64 a apply all rotations loslessly to a jpeg file (using exiftran)
377 root 1.90 ctrl-shift-t apply current rotation for future image loads
378 root 1.50 ctrl-v open a new visual schnauzer window for the current dir
379 root 1.63 ctrl-c clone the current image window
380 root 1.50 ctrl-e run an editor ($CV_EDITOR or "gimp") on the current image
381 root 1.57 ctrl-p fire up the print dialog
382 root 1.88 ctrl-shift-p same as ctrl-p, but automatically selects "ok"
383 root 1.50 escape cancel a crop action
384 root 1.11
385 root 1.33 And when playing movies, these additional keys are active:
386    
387 root 1.50 left rewind by 10 seconds
388     right forward by 10 seconds
389     down rewind by 60 seconds
390     up forward by 60 seconds
391     pg_up rewind by 600 seconds
392     pg_down forward by 600 seconds
393     o toggle on-screen display
394     p pause/unpause
395     escape stop playing
396     9 turn volume down
397     0 turn volume up
398 root 1.33
399 root 1.47 Any other keys will be sent to the default schnauzer window, which can be
400     toggled on and off by right-clicking into the image window.
401 root 1.11
402 root 1.47 Left-clicking into the image window will let you crop the image (usually
403     to zoom into large images that CV scales down).
404 root 1.11
405     =head2 THE VISUAL SCHNAUZER
406    
407 root 1.63 Any image-loading action in a schnauzer window acts on the
408     "last-recently-activated" imagewindow, which currently is simply the last
409     image window that received a keypress.
410    
411 root 1.11 You can use the following keys in the schnauzer window:
412    
413 root 1.47 ctrl-space,
414 root 1.50 space move to and display next image
415 root 1.47 ctrl-backspace,
416 root 1.50 backspace move to and display previous image
417 root 1.47 ctrl-return,
418 root 1.50 return display selected picture, or enter directory
419 root 1.11
420 root 1.50 cursor keys move selection
421     page-up move one page up
422     page-down move one page down
423     home move to first file
424     end move to last file
425    
426     ctrl-a select all files
427     ctrl-shift-a select all files currently displayed in the schnauzer window
428     ctrl-d delete selected files WITHOUT ASKING AGAIN
429 root 1.91 ctrl-g force generation of thumbnails for the selected files
430     ctrl-shift-g remove thumbnails for the selected files
431 root 1.50 ctrl-s rescan current direcory or files updates/deletes etc.
432     ctrl-u update selected (or all) icons if neccessary
433 root 1.76 ctrl-- unselected thumbnailed images
434     ctrl-+ keep only thumbnailed images, deselect others
435 root 1.47
436 root 1.56 ^ go to parent directory (caret).
437    
438 root 1.47 0-9,
439 root 1.50 a-z find the first filename beginning with this letter
440 root 1.47
441     Right-clicking into the schnauzer window displays a pop-up menu with
442     additional actions.
443    
444 root 1.60 =head3 SELECTION
445    
446     You can select entries in the Schnauzer in a variety of ways:
447    
448     =over 4
449    
450     =item Keyboard
451    
452     Moving the cursor with the keyboard will first deselect all files and then
453     select the file you moved to.
454    
455 root 1.61 =item Clicking
456 root 1.60
457 root 1.61 Clicking on an entry will select the one you clicked and deselect all
458     others.
459 root 1.60
460 root 1.61 =item Shift-Clicking
461    
462     Shift-clicking will toggle the selection on the entry under the mouse.
463    
464     =item Dragging
465 root 1.60
466     Dragging will select all entries between the one selected when pushing the
467     button and the one selected when releasing the button. If you move above
468     or below the schnauzer area while drag-selecting, the schnauzer will move
469     up/down one row twice per second. In addition, horizontal mouse movement
470     acts as a kind of invisible horizontal scrollbar.
471    
472     =item Hint: double-click works while click-selecting
473    
474     You can double-click any image while click-selecting to display it
475     without stopping the selection process. This will act as if you normally
476     double-clicked the image to display it, and will toggle the selection
477     twice, resulting in no change.
478    
479     =back
480    
481 root 1.47 =head1 FILES
482    
483     When starting, CV runs the F<.cvrc> file in your F<$HOME> directory as if
484     it were a perl script. in that, you will mostly load plug-ins.
485    
486     Example:
487    
488     system "fping -q -t 10 ether"
489     or require "/fs/cv/cvplugin.pl";
490    
491     This will load a plug-in, but only if the machine I<ether> is reachable
492     (supposedly the plug-in is networked in some way :).
493 root 1.11
494 root 1.79 =head1 ENVIRONMENT VARIABLES
495 root 1.27
496     =over 4
497    
498 root 1.38 =item CV_EDITOR
499    
500     The program that gets executed when the user presses C<CTRL-e> in the
501     Schnauzer or image window. The default is C<gimp>.
502    
503 root 1.70 =item CV_AUDIO_PLAYER
504    
505 root 1.94 EXPERIMENTAL: audio playback is now via mpv, this variable is currently
506     ignored.
507    
508 root 1.70 Program used to play all sorts of audio (wav, aif, mp3, ogg...), default "play".
509     Will be called like C<< $CV_AUDIO_PLAYER -- <path> >>.
510    
511 root 1.92 =item CV_MPLAYER
512    
513     Program used to play all sorts of video files. Unlike C<CV_AUDIO_PLAYER>,
514 root 1.99 this really must be some version of the C<mpv> programs, or something that
515     is very command-line compatible to them.
516 root 1.92
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