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