File Coverage

blib/lib/Win32/Shortkeys.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Win32::Shortkeys;
2             #use lib qw( U:/docs/perl/mod/hg_Win32-Shortkeys-Kbh/lib);
3            
4             =head1 NAME
5            
6             Win32::Shortkeys - A shortkeys perl script for windows
7            
8             =cut
9            
10             our $VERSION = '0.01';
11            
12             =head1 VERSION
13            
14             0.01
15            
16             =cut
17            
18 1     1   47216 use strict;
  1         3  
  1         23  
19 1     1   4 use warnings;
  1         1  
  1         19  
20 1     1   287 use Config::YAML::Tiny;
  1         5291  
  1         24  
21 1     1   101 use Win32::Clipboard;
  0            
  0            
22             use Win32::Shortkeys::Kbh qw(:all);
23             #require Win32::Shortkeys::Kbh;
24             #import Win32::Shortkeys::Kbh qw(:all);
25             #use Win32::Process;
26             use Win32::Shortkeys::Manager;
27             use XML::Parser;
28             #use Data::Dumper;
29             use Time::HiRes qw(usleep);
30             use Carp;
31             use Encode;
32            
33             #my %data;
34            
35             my %shk_use_clpbrd;
36            
37             sub new {
38             my ( $class, $file ) = @_;
39            
40             my $self = bless( {}, ref($class) || $class );
41             my $usage = <
42             usage Win32::Shortkeys->new(config_file);
43             END
44             die $usage unless ($file);
45            
46             #die VK_RETURN;
47             $self->{config} = Config::YAML::Tiny->new( config => $file );
48             return $self;
49             }
50            
51             sub run {
52             my $self = shift;
53             my $com_map = $self->{config}->get_vkcode_map
54             or confess("vkcode_map undefined");
55            
56             for my $k ( keys %$com_map ) {
57            
58             # print "$k: ", eval $com_map->{$k}, "\n";
59             $com_map->{$k} = eval $com_map->{$k} or confess($@);
60            
61             }
62            
63             #die eval $self->{config}->get_quit_key;
64             $self->{quit_key} = eval $self->{config}->get_quit_key or confess($@);
65             $self->{load_key} = eval $self->{config}->get_load_key or confess($@);
66             $self->{usleep_delay} = eval $self->{config}->get_usleep_delay
67             or confess($@);
68             $self->{com_map} = $com_map;
69             my $xml = $self->parse_file;
70             $self->{shkm} = Win32::Shortkeys::Manager->new( $xml );
71            
72             #$self->{shkm}->print_all;
73            
74             set_key_processor( sub { $self->process_key(@_); } );
75            
76             #set_key_processor(sub { $self->test(@_);});
77            
78             register_hook();
79            
80             msg_loop();
81            
82             }
83            
84             sub parse_file {
85             my $self = shift;
86             my $encoding = $self->{config}->get_file_encoding;
87             $encoding = ( $encoding ? $encoding : "UTF8" );
88             my $path = $self->{config}->get_file_path
89             or confess("path to shortkeys xml file undefined");
90            
91             # "<:raw:encoding($encoding):crlf:utf8",
92             # open( my $FH, "<:encoding($encoding)", $path ) or die "can't open file: $!";
93            
94             #binmode(STDOUT, ":encoding(utf8)");
95             my $p =
96             XML::Parser->new( ErrorContext => 2, ProtocolEncoding => 'UTF-8' );
97            
98             # 'Default' => \&MySubs::def,
99             # 'Final' => \&MySubs::final
100            
101             $p->setHandlers(
102             'Start' => \&MySubs::start,
103             'Char' => \&MySubs::char,
104             'End' => \&MySubs::end,
105            
106            
107             # 'Default' => \&MySubs::def
108             );
109            
110             #print "parsing\n";
111             eval {$p->parsefile($path); };
112             if ( $@ ){
113             $@ =~ s/at \/.*?$//s; # remove module line number
114             print "\nERROR in '$path':\n$@\n";
115            
116             } else {
117             print "'$path' parsed with success\n";
118            
119             }
120             return MySubs::get_data();
121            
122             }
123            
124            
125            
126             sub process_key {
127             my ( $self, $cup, $code, $alt, $ext ) = @_;
128            
129             return unless $cup; #process key released, not key pressed
130             # print "process_key : $code\n";
131            
132             if ( $code == $self->{quit_key} ) {
133             unregister_hook();
134             #Win32::Process::KillProcess( $$, -1 );
135             quit();
136            
137             }
138             elsif ( $code == $self->{load_key} ) {
139             #%data = ();
140             my $xml = $self->parse_file;
141             $self->{shkm} = Win32::Shortkeys::Manager->new( $xml );
142             $self->{shkm}->print_all;
143            
144             }
145             else {
146             # usleep($self->{usleep_delay});
147             $self->{shkm}->listen($code);
148             }
149            
150             if ( $self->{shkm}->is_ready ) {
151             my $shk = $self->{shkm}->get_shortkey;
152             unregister_hook();
153             if ( $shk_use_clpbrd{$shk} ) {
154             my $data = $self->{shkm}->get_data;
155             $data =~ s/\n/\015\012/g;
156             my $oct = Encode::encode("cp1250", $data);
157             Win32::Clipboard::Set($oct);
158             usleep( $self->{usleep_delay} );
159             # send length($shk) + 1 delete keys + ctrl + v
160             paste_from_clpb( length($shk) + 1 );
161             }
162             else {
163             my $rawdata = $self->{shkm}->get_data;
164            
165             # send_string("Key hitted " . chr ( $code ));
166             $self->parse_raw_data( $rawdata, $shk );
167            
168             }
169             register_hook();
170             # print ("ERROR in register_hook $@\n") if ($@);
171            
172             }
173             }
174            
175             sub parse_raw_data {
176             my ( $self, $raw, $shk ) = @_;
177             my @chunks = split( /#/, $raw );
178             $chunks[0] = $raw unless (@chunks); #do the loop below even if $raw is a zero length string
179             my $delkeys = length($shk) + 1;
180             my $pos = 0;
181             my $last = @chunks;
182             my %seen;
183             my %chunk_seen;
184             #print "last: $last\n";
185             #print "chunks: ", join( "*", @chunks ), "\n";
186            
187             # my ($com, $text, $has_next, $next, $how_much);
188             for my $raw (@chunks) {
189             $chunk_seen{ $pos++ } = 0;
190             }
191             $pos = 0;
192             for my $raw (@chunks) {
193             my $com = undef;
194             my $text = undef;
195             my $has_next = ( $pos + 1 < $last ? 1 : 0 );
196             my $next = ( $has_next ? $chunks[ $pos + 1 ] : undef );
197             my $how_much = 1;
198             my $seen = $pos;
199             #print "raw: ", ( defined $raw ? $raw : " undef " ), " has_next: ", $has_next,
200             # " pos: ", $pos, " next: ", ( $next ? $next : " undef" ), "\n";
201             if ( $pos == 0 ) {
202            
203             if ( !$raw && $has_next ) {
204             if ( !$next ) {
205             #print "jump over *", $next, "*\n";
206             next;
207             }
208             }
209             else {
210             # print "*** $raw $pos\n";
211             $text = $raw;
212             }
213             }
214             else {
215             #si l'élément en cours est vide et que le suivant existe
216             #la chaine contenait ##
217            
218             if ( !$raw && $has_next ) {
219             $text = "#";
220             if ( $next && $has_next ) {
221             $text = "#" . $next;
222             }
223             $seen = $pos + 1;
224             }
225             else
226             { #sinon c'est une commande éventuellement suivie par du texte
227             $how_much = substr( $raw, 1, 2 );
228             $com = substr( $raw, 0, 1 );
229             if ( length($raw) > 3 ) {
230             $text = substr( $raw, 3 );
231             }
232             }
233             }
234             next if $chunk_seen{$seen};
235             if ($com) {
236             if ($delkeys) {
237             usleep( $self->{usleep_delay} );
238             send_cmd( $delkeys, VK_BACK );
239             }
240            
241             #$com doit etre traduit par evmap \t
242             my $vkcode = $self->{com_map}->{$com};
243             send_cmd( $how_much, $vkcode );
244             }
245             if (defined $text) { # send the delkeys even is text is a zero length string
246             if ($delkeys) {
247             usleep( $self->{usleep_delay} );
248             send_cmd( $delkeys, VK_BACK );
249             }
250             send_string($text);
251             }
252             $chunk_seen{$seen} = 1;
253             $delkeys = 0;
254             } #for
255             continue { $pos++; }
256             }
257            
258             sub parse_raw_data_old {
259             my ( $self, $raw, $shk ) = @_;
260             my @chunks = split( /#/, $raw );
261            
262             #my $text;
263             my $delkeys = length($shk) + 1;
264            
265             #my $pos = 0;
266             #my $com = undef;
267            
268             my $last = @chunks;
269             #print "last: $last\n";
270             #print "chunks: ", join( "*", @chunks ), "\n";
271             my %com_map = %{ $self->{com_map} };
272            
273             #for $raw (@chunks) {
274             for ( my $pos = 0; $pos < $last; $pos++ ) {
275            
276             #my $raw= $chunks[$pos];
277             my $com = undef;
278             my $text = undef;
279             my $how_much = 1;
280             print "raw: ", ( $chunks[$pos] ? $chunks[$pos] : " undef " ),
281             " pos: $pos\n";
282            
283             #print "defined ", ( defined $chunks[$pos] ? $chunks[$pos] : " undef "), "\n";
284             #print "length ", (length $chunks[$pos] ? $chunks[$pos] : " length : 0 "), "\n";
285             if ( $pos == 0 ) {
286             if ( !$chunks[$pos] && ( $pos + 1 < $last ) ) {
287            
288             #die "here";
289             if ( !$chunks[ $pos + 1 ] ) {
290             print "jump over *", $chunks[ $pos + 1 ], "*\n";
291            
292             #$pos++;
293             next;
294             }
295             }
296             else {
297             $text = $chunks[$pos];
298            
299             }
300            
301             }
302             else {
303             #si l'élément en cours est vide et que le suivant existe
304             #la chaine contenait ##
305             if ( !$chunks[$pos] && ( $pos + 1 < $last ) ) {
306             $text = "#";
307            
308             # (copies.length>i+1 && copies[i+1].length()>0)
309             if ( $chunks[ $pos + 1 ] && ( $pos + 1 < $last ) ) {
310             $text = "#" . $chunks[ $pos + 1 ];
311            
312             }
313            
314             #print "jumpover *", $chunks[$pos + 1], "*\n";
315             $pos++;
316            
317             #next;
318             }
319             else
320             { #sinon c'est une commande éventuellement suivie par du texte
321             $how_much = substr( $chunks[$pos], 1, 2 );
322             $com = substr( $chunks[$pos], 0, 1 );
323             if ( length( $chunks[$pos] ) > 3 ) {
324             $text = substr( $chunks[$pos], 3 );
325             }
326             }
327             }
328             # print "text: ", ( $text ? $text : " undef" ), "\n";
329             # print "com: ", ( $com ? $com : " undef" ), "\n";
330             if ($com) {
331             if ($delkeys) {
332             usleep( $self->{usleep_delay} );
333             send_cmd( $delkeys, VK_BACK );
334             $delkeys = 0;
335             }
336            
337             #$com doit etre traduit par evmap \t
338             my $vkcode = $com_map{$com};
339            
340             #die $com;
341             send_cmd( $how_much, $vkcode );
342            
343             }
344             if ($text) {
345             # print "delkeys: $delkeys\n";
346             if ($delkeys) {
347             usleep( $self->{usleep_delay} );
348            
349             #die $delkeys;
350             send_cmd( $delkeys, VK_BACK );
351             }
352            
353             #usleep($self->{usleep_delay});
354             send_string($text);
355             $delkeys = 0;
356             }
357            
358             }
359             }
360            
361             package MySubs;
362             #use Data::Dumper;
363            
364             my $shk;
365             my $current_text;
366             my %data;
367             sub start {
368             my ( $p, $el, %atts ) = @_;
369            
370             my $key;
371             if ( $el eq "data" ) {
372             $shk = $atts{k};
373             $shk_use_clpbrd{$shk} = 0;
374             $current_text = undef;
375             if ( $atts{"use.ctrl_v"} ) {
376            
377             # push @clpelems, $shk;
378             $shk_use_clpbrd{$shk} = 1;
379             }
380            
381             }
382             elsif ( $el eq "dataref" ) {
383             $key = $atts{"id"};
384             $current_text .= $data{$key} if ( $data{$key} );
385            
386             }
387            
388             }
389            
390            
391             sub end {
392             my ( $p, $el ) = @_;
393             #if ( $current_text && $el eq "data" ) {
394             if ($el eq "data") {
395             # print "end\nshk : $shk : $current_text\n";
396             $data{$shk} = ( defined $current_text ? $current_text : "");
397             # if ( $shk eq "a" ) { print( "end : ", $current_text, "\n" ); }
398             $current_text = undef;
399            
400             }
401            
402             }
403            
404             sub char {
405             my ( $p, $s ) = @_;
406             return unless $shk;
407             # $s =~ s/^[\f\t ]+//; # Replace leading tab with nothing
408             # $s =~ s/[\f\t ]+$//; #don't substitute in order to preserve space between datarefs elements
409             $s =~ s/[\f\t ]+/ /g;
410             $s =~ s/^\n$//g unless ( $shk_use_clpbrd{$shk} );
411            
412             #print("char: $s L:", length($s), "\n");
413             $current_text .= $s if ( defined $s );
414            
415            
416             }
417            
418             sub get_data{
419             return \%data;
420             }
421            
422            
423             =head1 SYNOPSIS
424            
425             use Win32::Shortkeys;
426             my $s = Win32::Shortkeys->new("kbhook.properties");
427             $s->run;
428            
429             Depending on the the sorkeys.xml file, some keystroke are replaced with string or keys command (enter, tab, cursor right ...) taken from this file.
430            
431             =head1 DESCRIPTION
432            
433             Since the synopsis above is short, the main things to describe are in the file pass to Cnew($file)>.
434            
435             =head2 Properties file
436            
437             It must follow the Config::YAML::Tiny syntax. Mine looks like
438            
439             file_path: shortkeys_utf8.xml
440             file_encoding: UTF8
441             use_ctrl_v: 1
442             load_key: VK_HOME
443             quit_key: VK_F12
444             usleep_delay: 400_000
445             vkcode_map:
446             t: VK_TAB
447             e: VK_RETURN
448             d: VK_DOWN
449             l: VK_LEFT
450             r: VK_RIGHT
451             x: VK_BACK
452             s: VK_SHIFT
453             c: VK_CONTROL
454             a: VK_MENU
455             w: VK_SPACE
456             h: VK_HOM
457            
458             The key given in the load_key property is used to reload the shorkeys.xml file (without exiting the script).
459             The key given in the quit_key property is used to terminate the script.
460            
461             =head2 The xml file
462            
463             It's name is given by the C property.
464             It's xml syntax is:
465            
466            
467            
468            
469             Recent advances in biochemical and molecular diagnostics for the rapid detection of antibiotic-resistant Enterobacteriaceae: a focus
470            
471             Expert Review of Molecular Diagnostics
472            
473             ....
474            
475            
476            
477             The values of the k attribute are a-z string composed of lower case character(s) (a string can have two or more characters).
478             I call those strings shortkeys and when press on the keyboard after they < key with the script running, the key pressed are replaced by the content of the corresponding data element.
479            
480             For example, with the cursor in an opened notepad file, hitting the two keys
481             two characters with the value of the corresponding element: Expert Review of Molecular Diagnostics.
482            
483             The shortkeys.xml file should be utf-8 encoded, even if the encoding can be defined in the properties.
484            
485             With the key <, the script enter a "search mode" for a shortkey sequence. This key is hard coded and can't be changed (unless you edit the code).
486            
487             The text from the shortkeys file is sent to the keyboard using the send_input API function. With using the C attribute in a data element, the text will be place in the clipboard and paste (with sending the keys ctlr + v) at the cursor position.
488            
489            
490             This text will be copied and paste.
491             And the new line will be preserved.
492            
493            
494            
495             In the xml file, data elements can be combine using a dataref element.
496            
497             10.1080/14737159.2017.1289087
498            
499             Published version; http://dx.doi.org/
500            
501            
502             When hitting
503            
504             =head2 Commands syntax in shortkey.xml
505            
506             =over
507            
508             =item * a command keystroke start with # (to diplay # as a character, it has to be enter has ##), next you have to give
509            
510             =item * the command itself, set by a character (only one character) listed in the map defined with the property vkcode_map
511            
512             =item * how much you want to repeat that command, on two position, with a padding 0 if necessary (01)
513            
514             =item * the next characters are treated as text (unless a new command keystroke is defined with #)
515            
516             =item * The shift, control and alt keys are released
517            
518             =over
519            
520             =item * after a non-command key has been given. For example ctr+shift+a (written as #c01#s01a) will send the following event: key press for the keys control and shift, key press and released for the a key, key release for shift and control
521            
522             =item * at the end of a command keystroke, if the keys have not been released. For example a sequence of shit+tab, shift+tab, shift+tab (#s01#t01#t01#t01) will release the shift key at the end. On the contrary #s01#t01#t01#t01abc will call three back tab and will write Abc.
523            
524             =back
525            
526             =back
527            
528             =head1 INSTALLATION
529            
530             To install this module type the following:
531            
532             perl Makefile.PL
533             make
534             make test
535             make install
536            
537             On windows use nmake or dmake instead of make.
538            
539             =head1 DEPENDENCIES
540            
541             The following modules are required in order to use this module
542            
543             Test::Simple => 0.44,
544             Win32::Shortkeys::Kbh => 0.01,
545             Config::YAML::Tiny => 1.42,
546             Win32::Clipboard => 0.58,
547             XML::Parser => 2.44,
548             Encode => 2.84,
549             Time::HiRes => 1.9733,
550             Carp => 1.40
551            
552            
553             =head1 SUPPORT
554            
555             Any questions or problems can be posted to me (rappazf) on my gmail account.
556            
557             The current state of the source can be extract using Mercurial from
558            
559             L.
560            
561             =head1 AUTHOR
562            
563             FranEois Rappaz
564            
565            
566             =head1 COPYRIGHT
567            
568             This program is free software; you can redistribute
569             it and/or modify it under the same terms as Perl itself.
570            
571             The full text of the license can be found in the
572             LICENSE file included with this module.
573            
574            
575             =cut
576            
577             1;
578            
579            
580