ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/CV/bin/cv
Revision: 1.94
Committed: Sat Dec 23 18:53:36 2017 UTC (6 years, 5 months ago) by root
Branch: MAIN
CVS Tags: rel-1_71, rel-1_7
Changes since 1.93: +3 -0 lines
Log Message:
*** empty log message ***

File Contents

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