ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/CV/bin/cv
Revision: 1.99
Committed: Sat Apr 11 11:25:47 2020 UTC (4 years, 1 month ago) by root
Branch: MAIN
Changes since 1.98: +2 -6 lines
Log Message:
*** empty log message ***

File Contents

# Content
1 #!/opt/bin/perl
2
3 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 BEGIN {
12 require Gtk2::CV::Plugin;
13 require "$ENV{HOME}/.cvrc" if -r "$ENV{HOME}/.cvrc";
14 }
15
16 use common::sense;
17
18 use Cwd ();
19 use Encode ();
20 use File::Glob ();
21 use Scalar::Util ();
22
23 use Gtk2;
24 use Gtk2::Gdk::Keysyms;
25
26 use Gtk2::CV;
27
28 use Gtk2::CV::ImageWindow;
29 use Gtk2::CV::Schnauzer;
30
31 use Carp (); $Carp::MaxArgLen = 256;
32
33 use Gtk2::CV::Plugin::NameCluster;
34 use Gtk2::CV::Plugin::RCluster;
35 use Gtk2::CV::Plugin::PatRenamer;
36 use Gtk2::CV::Plugin::MetaCluster;
37
38 use AnyEvent::Fork::Template;
39
40 Gtk2::CV::Jobber::set_template $AnyEvent::Fork::Template;
41
42 # now we can initialize Gtk2 etc.
43 init Gtk2;
44
45 Gtk2::Rc->parse (Gtk2::CV::find_rcfile "gtkrc");
46
47 use File::Spec;
48
49 my $mainwin;
50 my $viewer;
51 my $viewer_count;
52 my $schnauzer;
53 my $info;
54 my $help;
55
56 my $schnauzer_idx = 0;
57
58 sub new_schnauzer {
59 my $s = new Gtk2::CV::Schnauzer;
60
61 $s->signal_connect_after (key_press_event => \&std_keys);
62 $s->signal_connect (activate => sub {
63 my $label = sprintf "%s (%d)",
64 (Glib::filename_display_name +(File::Spec->splitpath ($_[1]))[2]),
65 -s $_[1];
66 $info->set_label ($label);
67 $viewer->load_image ($_[1]) if $viewer; # TODO: error, or chose ANY viewer
68 });
69
70 Gtk2::CV::Plugin->call (new_schnauzer => $s);
71
72 $s
73 }
74
75 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 sub new_viewer {
86 my $self = new Gtk2::CV::ImageWindow;
87
88 Scalar::Util::weaken ($VIEWER{$self+0} = $self);
89
90 $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 $self->signal_connect (delete_event => sub { $_[0]->destroy; 0 });
111 $self->signal_connect (destroy => sub {
112 delete $VIEWER{$_[0]+0};
113 $viewer = undef if $viewer == $_[0];
114
115 main_quit Gtk2 unless --$viewer_count;
116
117 0
118 });
119
120 $self->signal_connect (button3_press_event => sub {
121 $mainwin->visible
122 ? $mainwin->hide
123 : $mainwin->show_all;
124
125 1
126 });
127
128 Gtk2::CV::Plugin->call (new_imagewindow => $self);
129
130 $self
131 }
132
133 sub std_keys {
134 my $key = $_[1]->keyval;
135 my $state = $_[1]->state;
136
137 my $ctrl = $state * "control-mask";
138
139 if ($key == $Gtk2::Gdk::Keysyms{q}) {
140 $viewer->destroy;
141 } elsif ($ctrl && $key == $Gtk2::Gdk::Keysyms{v}) {
142 my $w = new Gtk2::Window;
143
144 $w->set_role ("schnauzer");
145 $w->set_title ("CV: Schnauzer");
146 $w->add (my $s = new_schnauzer);
147 $s->set_dir (File::Spec->curdir);
148 $s->set_geometry_hints;
149 $w->show_all;
150
151 } elsif ($ctrl && $key == $Gtk2::Gdk::Keysyms{h}) {
152 unless ($help) {
153 require Gtk2::Ex::PodViewer;
154
155 $help = new Gtk2::Window;
156 $help->set_role ("help");
157 $help->set_title ("CV: Help");
158 $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 $sw->add (my $h = new Gtk2::Ex::PodViewer);
163
164 #binmode DATA, ":utf8";
165 $h->load_string (do { local $/; <DATA> });
166 }
167
168 $help->show_all;
169 } elsif (!$state && $Gtk2::Gdk::Keysyms{a} <= $key && $key <= $Gtk2::Gdk::Keysyms{z}) {
170 #
171 } else {
172 return 0;
173 }
174
175 1
176 }
177
178 &cvrc_boot if defined &cvrc_boot;
179
180 {
181 $viewer = new_viewer;
182 $::cur_viewer = $viewer;
183
184 $schnauzer = new_schnauzer;
185
186 $mainwin = new Gtk2::Window;
187 $mainwin->set_role ("main");
188 $mainwin->set_title ("CV");
189 $mainwin->add (my $vbox = new Gtk2::VBox);
190 $mainwin->signal_connect (delete_event => sub { $mainwin->hide; 1 });
191
192 $vbox->add ($schnauzer);
193 $vbox->pack_end (my $frame = new Gtk2::Frame, 0, 0, 0);
194 $frame->add (my $hbox = new Gtk2::HBox 0, 0);
195 $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
201 $schnauzer->set_geometry_hints;
202 }
203
204 if (@ARGV) {
205 my $show_first = sub {
206 $schnauzer->show_all;
207
208 # activate first file, but avoid dirs
209 my $entry = $schnauzer->{entry}[0];
210 my $path = "$entry->[0]/$entry->[1]";
211
212 $schnauzer->handle_key ($Gtk2::Gdk::Keysyms{space}, new Gtk2::Gdk::ModifierType [])
213 unless -d $path;
214
215 $viewer->show_all;
216 };
217
218 if (@ARGV == 1 && $ARGV[0] eq "-0r") {
219 local $/;
220 $schnauzer->set_paths ([split /\x00/, <STDIN>], 1, $show_first);
221 } elsif (@ARGV == 1 && -d $ARGV[0]) {
222 $schnauzer->set_dir (shift, $show_first);
223 } else {
224 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 $schnauzer->set_paths ([@ARGV], 1, $show_first);
229 }
230 } else {
231 $schnauzer->set_dir (File::Spec->curdir, sub {
232 $mainwin->show_all;
233 $viewer->show_all;
234 });
235 }
236
237 &cvrc_start if defined &cvrc_start;
238
239 main Gtk2;
240
241 Gtk2::CV::flush_aio;
242
243 __DATA__
244
245 =encoding utf-8
246
247 =head1 NAME
248
249 cv - a fast gtk+ image viewer loosely modeled after XV
250
251 =head1 SYNOPSIS
252
253 cv
254
255 cv directory
256
257 cv path...
258
259 cv -g <glob expression...>
260
261 find .. -print0 | cv -0r
262
263 =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 The directory scanning in CV uses some tricks that - on most modern
273 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 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 =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 =item * use of asynchronous I/O
292
293 CV tries to use asynchronous I/O whereever it makes sense, for example
294 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
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 correction, although this might change) to achieve this speed.
305
306 =item * fast thumbnail creation
307
308 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
313 =item * minimum optical clutter
314
315 CV has no menus or other user interface elements that take up a lot of
316 screen space (or are useful for beginning users). The schnauzer windows
317 can also be somewhat crowded.
318
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 =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 =item * i18n'ed filename handling throughout
335
336 As long as glib can recognize your filename encoding (either UTF-8 or
337 locale-specific, depending on the setting of G_BROKEN_FILENAMES) and you
338 have the relevant fonts, CV will display your filenames correctly.
339
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 =head1 DESCRIPTION
355
356 =head2 THE IMAGE WINDOW
357
358 You can use the following keys in the image window:
359
360 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 ctrl-sift-m toggle using current image size as max image size
370 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 a apply all rotations loslessly to a jpeg file (using exiftran)
377 ctrl-shift-t apply current rotation for future image loads
378 ctrl-v open a new visual schnauzer window for the current dir
379 ctrl-c clone the current image window
380 ctrl-e run an editor ($CV_EDITOR or "gimp") on the current image
381 ctrl-p fire up the print dialog
382 ctrl-shift-p same as ctrl-p, but automatically selects "ok"
383 escape cancel a crop action
384
385 And when playing movies, these additional keys are active:
386
387 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
399 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
402 Left-clicking into the image window will let you crop the image (usually
403 to zoom into large images that CV scales down).
404
405 =head2 THE VISUAL SCHNAUZER
406
407 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 You can use the following keys in the schnauzer window:
412
413 ctrl-space,
414 space move to and display next image
415 ctrl-backspace,
416 backspace move to and display previous image
417 ctrl-return,
418 return display selected picture, or enter directory
419
420 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 ctrl-g force generation of thumbnails for the selected files
430 ctrl-shift-g remove thumbnails for the selected files
431 ctrl-s rescan current direcory or files updates/deletes etc.
432 ctrl-u update selected (or all) icons if neccessary
433 ctrl-- unselected thumbnailed images
434 ctrl-+ keep only thumbnailed images, deselect others
435
436 ^ go to parent directory (caret).
437
438 0-9,
439 a-z find the first filename beginning with this letter
440
441 Right-clicking into the schnauzer window displays a pop-up menu with
442 additional actions.
443
444 =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 =item Clicking
456
457 Clicking on an entry will select the one you clicked and deselect all
458 others.
459
460 =item Shift-Clicking
461
462 Shift-clicking will toggle the selection on the entry under the mouse.
463
464 =item Dragging
465
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 =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
494 =head1 ENVIRONMENT VARIABLES
495
496 =over 4
497
498 =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 =item CV_AUDIO_PLAYER
504
505 EXPERIMENTAL: audio playback is now via mpv, this variable is currently
506 ignored.
507
508 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 =item CV_MPLAYER
512
513 Program used to play all sorts of video files. Unlike C<CV_AUDIO_PLAYER>,
514 this really must be some version of the C<mpv> programs, or something that
515 is very command-line compatible to them.
516
517 Note: for video-thumbnailing, mplayer is still used (and hardcoded).
518
519 =item CV_PRINT_DESTINATION
520
521 The default (perl-style) destination to use in the print dialog.
522
523 =item CV_TRASHCAN
524
525 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
530 =back
531
532 =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 =head1 SECURITY CONSIDERATIONS
540
541 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
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 =head1 BUGS/TODO
550
551 Lots of functionality is missing.
552
553 Pixbuf doesn't always honor G_BROKEN_FILENAMES, so accessing files with
554 names incompatible with utf-8 might fail.
555
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