ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/cvsroot/CV/bin/cv
Revision: 1.105
Committed: Wed Aug 2 21:56:44 2023 UTC (3 years, 2 months ago) by root
Branch: MAIN
Changes since 1.104: +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.104 use Gtk2; qw(-init -threads-init);
12     BEGIN { Gtk2::Gdk::Threads->enter };
13 root 1.103
14 root 1.98 BEGIN {
15     require Gtk2::CV::Plugin;
16     require "$ENV{HOME}/.cvrc" if -r "$ENV{HOME}/.cvrc";
17     }
18    
19 root 1.80 use common::sense;
20    
21 root 1.37 use Cwd ();
22     use Encode ();
23 root 1.72 use File::Glob ();
24 root 1.79 use Scalar::Util ();
25 root 1.37
26 root 1.12 use Gtk2::Gdk::Keysyms;
27    
28 root 1.48 use Gtk2::CV;
29    
30 root 1.12 use Gtk2::CV::ImageWindow;
31     use Gtk2::CV::Schnauzer;
32    
33 root 1.97 use Carp (); $Carp::MaxArgLen = 256;
34 root 1.82
35 root 1.48 use Gtk2::CV::Plugin::NameCluster;
36     use Gtk2::CV::Plugin::RCluster;
37 root 1.81 use Gtk2::CV::Plugin::PatRenamer;
38 root 1.96 use Gtk2::CV::Plugin::MetaCluster;
39 root 1.16
40 root 1.86 use AnyEvent::Fork::Template;
41    
42 root 1.87 Gtk2::CV::Jobber::set_template $AnyEvent::Fork::Template;
43    
44     # now we can initialize Gtk2 etc.
45     init Gtk2;
46 root 1.77
47 root 1.16 Gtk2::Rc->parse (Gtk2::CV::find_rcfile "gtkrc");
48    
49 root 1.20 use File::Spec;
50    
51     my $mainwin;
52 root 1.12 my $viewer;
53 root 1.62 my $viewer_count;
54 root 1.12 my $schnauzer;
55 root 1.20 my $info;
56 root 1.21 my $help;
57 root 1.12
58 root 1.25 my $schnauzer_idx = 0;
59    
60 root 1.12 sub new_schnauzer {
61 root 1.20 my $s = new Gtk2::CV::Schnauzer;
62 root 1.12
63     $s->signal_connect_after (key_press_event => \&std_keys);
64 root 1.20 $s->signal_connect (activate => sub {
65     my $label = sprintf "%s (%d)",
66 root 1.75 (Glib::filename_display_name +(File::Spec->splitpath ($_[1]))[2]),
67 root 1.20 -s $_[1];
68     $info->set_label ($label);
69 root 1.62 $viewer->load_image ($_[1]) if $viewer; # TODO: error, or chose ANY viewer
70 root 1.20 });
71 root 1.12
72 root 1.40 Gtk2::CV::Plugin->call (new_schnauzer => $s);
73    
74 root 1.53 $s
75 root 1.12 }
76    
77 root 1.79 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 root 1.62 sub new_viewer {
88     my $self = new Gtk2::CV::ImageWindow;
89    
90 root 1.79 Scalar::Util::weaken ($VIEWER{$self+0} = $self);
91    
92 root 1.62 $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 root 1.63 $self->signal_connect (delete_event => sub { $_[0]->destroy; 0 });
113 root 1.62 $self->signal_connect (destroy => sub {
114 root 1.79 delete $VIEWER{$_[0]+0};
115 root 1.62 $viewer = undef if $viewer == $_[0];
116    
117     main_quit Gtk2 unless --$viewer_count;
118 root 1.63
119     0
120 root 1.62 });
121    
122     $self->signal_connect (button3_press_event => sub {
123     $mainwin->visible
124     ? $mainwin->hide
125     : $mainwin->show_all;
126 root 1.63
127 root 1.62 1
128     });
129    
130     Gtk2::CV::Plugin->call (new_imagewindow => $self);
131    
132     $self
133     }
134    
135 root 1.12 sub std_keys {
136     my $key = $_[1]->keyval;
137     my $state = $_[1]->state;
138    
139 root 1.21 my $ctrl = $state * "control-mask";
140 root 1.12
141     if ($key == $Gtk2::Gdk::Keysyms{q}) {
142 root 1.62 $viewer->destroy;
143 root 1.12 } elsif ($ctrl && $key == $Gtk2::Gdk::Keysyms{v}) {
144 root 1.20 my $w = new Gtk2::Window;
145 root 1.26
146 root 1.66 $w->set_role ("schnauzer");
147 root 1.26 $w->set_title ("CV: Schnauzer");
148 root 1.20 $w->add (my $s = new_schnauzer);
149     $s->set_dir (File::Spec->curdir);
150 root 1.25 $s->set_geometry_hints;
151 root 1.21 $w->show_all;
152 root 1.25
153 root 1.21 } elsif ($ctrl && $key == $Gtk2::Gdk::Keysyms{h}) {
154     unless ($help) {
155 root 1.73 require Gtk2::Ex::PodViewer;
156 root 1.21
157     $help = new Gtk2::Window;
158 root 1.67 $help->set_role ("help");
159 root 1.26 $help->set_title ("CV: Help");
160 root 1.21 $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 root 1.73 $sw->add (my $h = new Gtk2::Ex::PodViewer);
165 root 1.21
166     #binmode DATA, ":utf8";
167     $h->load_string (do { local $/; <DATA> });
168     }
169    
170     $help->show_all;
171 root 1.83 } elsif (!$state && $Gtk2::Gdk::Keysyms{a} <= $key && $key <= $Gtk2::Gdk::Keysyms{z}) {
172 root 1.82 #
173 root 1.12 } else {
174 root 1.43 return 0;
175 root 1.12 }
176    
177 root 1.43 1
178 root 1.12 }
179    
180 root 1.98 &cvrc_boot if defined &cvrc_boot;
181    
182 root 1.20 {
183 root 1.62 $viewer = new_viewer;
184     $::cur_viewer = $viewer;
185 root 1.40
186 root 1.20 $schnauzer = new_schnauzer;
187    
188     $mainwin = new Gtk2::Window;
189 root 1.66 $mainwin->set_role ("main");
190 root 1.20 $mainwin->set_title ("CV");
191     $mainwin->add (my $vbox = new Gtk2::VBox);
192 root 1.53 $mainwin->signal_connect (delete_event => sub { $mainwin->hide; 1 });
193 root 1.20
194     $vbox->add ($schnauzer);
195 root 1.22 $vbox->pack_end (my $frame = new Gtk2::Frame, 0, 0, 0);
196 root 1.20 $frame->add (my $hbox = new Gtk2::HBox 0, 0);
197 root 1.45 $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 root 1.25
203     $schnauzer->set_geometry_hints;
204 root 1.20 }
205 root 1.12
206     if (@ARGV) {
207 root 1.68 my $show_first = sub {
208     $schnauzer->show_all;
209 root 1.85
210     # activate first file, but avoid dirs
211     my $entry = $schnauzer->{entry}[0];
212     my $path = "$entry->[0]/$entry->[1]";
213    
214 root 1.89 $schnauzer->handle_key ($Gtk2::Gdk::Keysyms{space}, new Gtk2::Gdk::ModifierType [])
215 root 1.85 unless -d $path;
216    
217 root 1.68 $viewer->show_all;
218     };
219    
220 root 1.102 my $force_sort = $ARGV[0] eq "--sort" ? shift @ARGV : 0;
221    
222 root 1.71 if (@ARGV == 1 && $ARGV[0] eq "-0r") {
223     local $/;
224 root 1.102 $schnauzer->set_paths ([split /\x00/, <STDIN>], !$force_sort, $show_first);
225 root 1.71 } elsif (@ARGV == 1 && -d $ARGV[0]) {
226 root 1.74 $schnauzer->set_dir (shift, $show_first);
227 root 1.71 } else {
228 root 1.72 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 root 1.102 $schnauzer->set_paths ([@ARGV], !$force_sort, $show_first);
233 root 1.71 }
234 root 1.17 } else {
235 root 1.68 $schnauzer->set_dir (File::Spec->curdir, sub {
236     $mainwin->show_all;
237     $viewer->show_all;
238     });
239 root 1.12 }
240    
241 root 1.98 &cvrc_start if defined &cvrc_start;
242    
243 root 1.12 main Gtk2;
244 root 1.21
245 root 1.69 Gtk2::CV::flush_aio;
246    
247 root 1.21 __DATA__
248 root 1.12
249 root 1.58 =encoding utf-8
250    
251 root 1.11 =head1 NAME
252    
253 root 1.47 cv - a fast gtk+ image viewer loosely modeled after XV
254 root 1.11
255     =head1 SYNOPSIS
256    
257 root 1.84 cv
258    
259     cv directory
260    
261     cv path...
262    
263     cv -g <glob expression...>
264    
265     find .. -print0 | cv -0r
266 root 1.11
267 root 1.102 cv --sort ...
268    
269 root 1.47 =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 root 1.55 The directory scanning in CV uses some tricks that - on most modern
279 root 1.47 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 root 1.55 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 root 1.53 =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 root 1.47 =item * use of asynchronous I/O
298    
299     CV tries to use asynchronous I/O whereever it makes sense, for example
300 root 1.53 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 root 1.47
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 root 1.55 correction, although this might change) to achieve this speed.
311 root 1.47
312     =item * fast thumbnail creation
313    
314 root 1.55 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 root 1.47
319     =item * minimum optical clutter
320    
321     CV has no menus or other user interface elements that take up a lot of
322 root 1.55 screen space (or are useful for beginning users). The schnauzer windows
323     can also be somewhat crowded.
324 root 1.47
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 root 1.55 =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 root 1.47 =item * i18n'ed filename handling throughout
341    
342     As long as glib can recognize your filename encoding (either UTF-8 or
343 root 1.55 locale-specific, depending on the setting of G_BROKEN_FILENAMES) and you
344     have the relevant fonts, CV will display your filenames correctly.
345 root 1.47
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 root 1.11 =head1 DESCRIPTION
361    
362     =head2 THE IMAGE WINDOW
363    
364     You can use the following keys in the image window:
365    
366 root 1.50 q quit the program
367     < half the image size
368     > double the image size
369 root 1.101 , shrink the image by ~9% (opposite of .)
370 root 1.50 . 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 root 1.105 ctrl-shift-m toggle using current image size as max image size
376 root 1.50 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 root 1.64 a apply all rotations loslessly to a jpeg file (using exiftran)
382 root 1.90 ctrl-shift-t apply current rotation for future image loads
383 root 1.50 ctrl-v open a new visual schnauzer window for the current dir
384 root 1.63 ctrl-c clone the current image window
385 root 1.50 ctrl-e run an editor ($CV_EDITOR or "gimp") on the current image
386 root 1.57 ctrl-p fire up the print dialog
387 root 1.88 ctrl-shift-p same as ctrl-p, but automatically selects "ok"
388 root 1.50 escape cancel a crop action
389 root 1.11
390 root 1.33 And when playing movies, these additional keys are active:
391    
392 root 1.50 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 root 1.33
404 root 1.47 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 root 1.11
407 root 1.47 Left-clicking into the image window will let you crop the image (usually
408     to zoom into large images that CV scales down).
409 root 1.11
410     =head2 THE VISUAL SCHNAUZER
411    
412 root 1.63 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 root 1.11 You can use the following keys in the schnauzer window:
417    
418 root 1.47 ctrl-space,
419 root 1.50 space move to and display next image
420 root 1.47 ctrl-backspace,
421 root 1.50 backspace move to and display previous image
422 root 1.47 ctrl-return,
423 root 1.50 return display selected picture, or enter directory
424 root 1.11
425 root 1.50 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 root 1.91 ctrl-g force generation of thumbnails for the selected files
435     ctrl-shift-g remove thumbnails for the selected files
436 root 1.50 ctrl-s rescan current direcory or files updates/deletes etc.
437     ctrl-u update selected (or all) icons if neccessary
438 root 1.76 ctrl-- unselected thumbnailed images
439     ctrl-+ keep only thumbnailed images, deselect others
440 root 1.47
441 root 1.56 ^ go to parent directory (caret).
442    
443 root 1.103 currently disabled:
444 root 1.47 0-9,
445 root 1.50 a-z find the first filename beginning with this letter
446 root 1.47
447     Right-clicking into the schnauzer window displays a pop-up menu with
448     additional actions.
449    
450 root 1.60 =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 root 1.61 =item Clicking
462 root 1.60
463 root 1.61 Clicking on an entry will select the one you clicked and deselect all
464     others.
465 root 1.60
466 root 1.61 =item Shift-Clicking
467    
468     Shift-clicking will toggle the selection on the entry under the mouse.
469    
470     =item Dragging
471 root 1.60
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 root 1.47 =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 root 1.11
500 root 1.79 =head1 ENVIRONMENT VARIABLES
501 root 1.27
502     =over 4
503    
504 root 1.38 =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 root 1.70 =item CV_AUDIO_PLAYER
510    
511 root 1.94 EXPERIMENTAL: audio playback is now via mpv, this variable is currently
512     ignored.
513    
514 root 1.70 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 root 1.92 =item CV_MPLAYER
518    
519     Program used to play all sorts of video files. Unlike C<CV_AUDIO_PLAYER>,
520 root 1.99 this really must be some version of the C<mpv> programs, or something that
521     is very command-line compatible to them.
522 root 1.92
523 root 1.93 Note: for video-thumbnailing, mplayer is still used (and hardcoded).
524    
525 root 1.27 =item CV_PRINT_DESTINATION
526    
527     The default (perl-style) destination to use in the print dialog.
528    
529 root 1.38 =item CV_TRASHCAN
530    
531 root 1.78 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 root 1.38
536 root 1.27 =back
537    
538 root 1.79 =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 root 1.23 =head1 SECURITY CONSIDERATIONS
546    
547 root 1.54 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 root 1.23
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 root 1.11 =head1 BUGS/TODO
556 root 1.23
557 root 1.47 Lots of functionality is missing.
558    
559     Pixbuf doesn't always honor G_BROKEN_FILENAMES, so accessing files with
560 root 1.54 names incompatible with utf-8 might fail.
561 root 1.11
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 root 1.1