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