1#!/usr/bin/env perl
2# $XTermId: modify-keys.pl,v 1.92 2022/11/24 12:43:26 tom Exp $
3# -----------------------------------------------------------------------------
4# this file is part of xterm
5#
6# Copyright 2019-2020,2022 by Thomas E. Dickey
7#
8#                         All Rights Reserved
9#
10# Permission is hereby granted, free of charge, to any person obtaining a
11# copy of this software and associated documentation files (the
12# "Software"), to deal in the Software without restriction, including
13# without limitation the rights to use, copy, modify, merge, publish,
14# distribute, sublicense, and/or sell copies of the Software, and to
15# permit persons to whom the Software is furnished to do so, subject to
16# the following conditions:
17#
18# The above copyright notice and this permission notice shall be included
19# in all copies or substantial portions of the Software.
20#
21# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
22# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
24# IN NO EVENT SHALL THE ABOVE LISTED COPYRIGHT HOLDER(S) BE LIABLE FOR ANY
25# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
26# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
27# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28#
29# Except as contained in this notice, the name(s) of the above copyright
30# holders shall not be used in advertising or otherwise to promote the
31# sale, use or other dealings in this Software without prior written
32# authorization.
33# -----------------------------------------------------------------------------
34# Print a table to illustrate the modifyOtherKeys resource choices.
35#
36# Some of the key combinations are unavailable unless certain translations
37# resource settings are suppressed.  This command helped to verify those:
38#	xterm -xrm '*omitTranslation:fullscreen,scroll-lock,shift-fonts'
39#
40# Additionally, a test-script was written to exercise xterm when the
41# "Allow SendEvents" feature is enabled, in combination with keys sent by
42# commands like this:
43#	xdotool key --window XXX shift 2>/dev/null
44#
45# A curses application running in the target xterm showed the received data
46# in the terminfo-style format used in this script.
47
48# TODO factor in the backspace/delete meta/alt/escape resource-settings
49# TODO show keycodes via "xmodmap -pk" as alternative to xkbcomp
50# TODO show different sort-order (code, sym, xkb)
51# TODO use U+xxxx codepoints in keysymdef.h for rendering plain text
52# TODO optionally show 2**N, e.g., 4 (shift+control), 8 (shift+alt+control) or 16 (+meta) modifiers
53# TODO optionally show -c (cursor) -e (edit) -f (function-keys) with modifiers
54
55use strict;
56use warnings;
57
58use Getopt::Std;
59
60$| = 1;
61
62our ( $opt_d, $opt_h, $opt_k, $opt_K, $opt_l, $opt_m, $opt_o, $opt_u, $opt_v );
63
64our $REPORT;
65our @headers;
66our @nolinks = ();
67our ( $xkb_layout, $xkb_model );
68our $keyfile = "/usr/include/X11/keysymdef.h";
69
70our @keyNames;    # xkb's notion of key-names (undocumented)
71our %keySyms;     # all keysyms, hashed by name
72our %keyCodes;    # all keysyms, hashed by keycode
73our %uniCodes;    # keysym Unicode values, hashed by keycode
74our %uniNames;    # keysym Unicode descriptions, hashed by keycode
75our @keyTypes;    # XkbKeyTypeRec
76our @symCache;    # keysyms defined in keysymdef.h which might be used
77our @symMap;      # index into symCache from keyNames
78our %keysUsed;    # report derived from @symMap, etc.
79our %linkUsed;    # check for uniqueness of html anchor-names
80
81our $MAXMODS = 8; # maximum for modifier-param
82our %Shifted;     # map keycode to shifted-keycode seen by xterm
83
84# imitate /usr/include/X11/X.h
85our $ShiftMask   = 1;
86our $LockMask    = 2;
87our $ControlMask = 4;
88our $AltMask     = 8;     # assume mod1=alt
89our $MetaMask    = 16;    # assume mod2=meta
90
91our %editKeys = qw(
92  XK_Delete             1
93  XK_Prior              1
94  XK_Next               1
95  XK_Insert             1
96  XK_Find               1
97  XK_Select             1
98  XK_KP_Delete          1
99  XK_KP_Insert          1
100  XK_ISO_Left_Tab       1
101);
102
103sub failed($) {
104    printf STDERR "%s\n", $_[0];
105    exit 1;
106}
107
108# prefer hex with 4 digit for hash keys
109sub toCode($) {
110    my $value = shift;
111    $value = sprintf( "0x%04x", $value ) if ( $value =~ /^\d+$/ );
112    return $value;
113}
114
115sub codeOf($) {
116    my $value  = shift;
117    my $result = 0;
118    &failed("missing keysym") unless ( defined $value );
119    if ( $value =~ /^\d+$/ ) {
120        $result = $value;
121    }
122    elsif ( $value =~ /^0x[[:xdigit:]]+$/i ) {
123        $result = hex $value;
124    }
125    elsif ( $value =~ /^XK_/ ) {
126        $result = hex $keySyms{$value};
127    }
128    else {
129        &failed("not a keysym: $value");
130    }
131    return $result;
132}
133
134# macros from <X11/Xutil.h>
135
136sub IsKeypadKey($) {
137    my $code = &codeOf( $_[0] );
138    my $result = ( ( $code >= &codeOf("XK_KP_Space") )
139          and ( $code <= &codeOf("XK_KP_Equal") ) ) ? 1 : 0;
140    return $result;
141}
142
143sub IsPrivateKeypadKey($) {
144    my $code = &codeOf( $_[0] );
145    my $result =
146      ( ( $code >= 0x11000000 ) and ( $code <= 0x1100FFFF ) ) ? 1 : 0;
147    return $result;
148}
149
150sub IsCursorKey($) {
151    my $code = &codeOf( $_[0] );
152    my $result =
153      ( ( $code >= &codeOf("XK_Home") ) and ( $code < &codeOf("XK_Select") ) )
154      ? 1
155      : 0;
156    return $result;
157}
158
159sub IsPFKey($) {
160    my $code = &codeOf( $_[0] );
161    my $result =
162      ( ( $code >= &codeOf("XK_KP_F1") ) and ( $code <= &codeOf("XK_KP_F4") ) )
163      ? 1
164      : 0;
165    return $result;
166}
167
168sub IsFunctionKey($) {
169    my $code = &codeOf( $_[0] );
170    my $result =
171      ( ( $code >= &codeOf("XK_F1") ) and ( $code <= &codeOf("XK_F35") ) )
172      ? 1
173      : 0;
174    return $result;
175}
176
177sub IsMiscFunctionKey($) {
178    my $code = &codeOf( $_[0] );
179    my $result =
180      ( ( $code >= &codeOf("XK_Select") ) and ( $code <= &codeOf("XK_Break") ) )
181      ? 1
182      : 0;
183    return $result;
184}
185
186sub IsModifierKey($) {
187    my $code   = &codeOf( $_[0] );
188    my $result = (
189        (
190                  ( $code >= &codeOf("XK_Shift_L") )
191              and ( $code <= &codeOf("XK_Hyper_R") )
192        )
193          or (  ( $code >= &codeOf("XK_ISO_Lock") )
194            and ( $code <= &codeOf("XK_ISO_Level5_Lock") ) )
195          or ( $code == &codeOf("XK_Mode_switch") )
196          or ( $code == &codeOf("XK_Num_Lock") )
197    ) ? 1 : 0;
198    return $result;
199}
200
201# debugging/reporting
202
203# Xutil.h's macros do not cover the whole range of special keys, which are not
204# actually printable.
205sub IsSpecialKey($) {
206    my $code = &codeOf( $_[0] );
207    my $result =
208      ( ( $code >= 0xff00 ) and ( $code <= 0xffff ) )
209      ? 1
210      : 0;
211    return $result;
212}
213
214sub VisibleChar($) {
215    my $ch     = shift;
216    my $ord    = ord $ch;
217    my $result = $ch;
218    if ( $ord < 32 ) {
219        if ( $ord == 8 ) {
220            $result = '\b';
221        }
222        elsif ( $ord == 9 ) {
223            $result = '\t';
224        }
225        elsif ( $ord == 10 ) {
226            $result = '\n';
227        }
228        elsif ( $ord == 12 ) {
229            $result = '\f';
230        }
231        elsif ( $ord == 13 ) {
232            $result = '\r';
233        }
234        elsif ( $ord == 27 ) {
235            $result = '\E';
236        }
237        else {
238            $result = sprintf( "^%c", $ord + 64 );
239        }
240    }
241    elsif ( $ord == 32 ) {
242        $result = '\s';
243    }
244    elsif ( $ord == 94 ) {
245        $result = '\^';
246    }
247    elsif ( $ord == 92 ) {
248        $result = '\\\\';
249    }
250    elsif ( $ord == 127 ) {
251        $result = '^?';
252    }
253    return $result;
254}
255
256sub IsShift($$) {
257    my $code   = shift;
258    my $state  = shift;    # 0/1=normal, 2=shift
259    my $result = 0;
260    if ( ( ( $state - 1 ) & 1 ) != 0 ) {
261        if ( $Shifted{$code} ) {
262            return 1 if ( $Shifted{$code} ne $code );
263        }
264    }
265    return 0;
266}
267
268sub TypeOf($) {
269    my $code   = &toCode( $_[0] );
270    my $result = "other";
271    $result = "special"  if ( &IsSpecialKey($code) );
272    $result = "keypad"   if ( &IsKeypadKey($code) );
273    $result = "*keypad"  if ( &IsPrivateKeypadKey($code) );
274    $result = "cursor"   if ( &IsCursorKey($code) );
275    $result = "pf-key"   if ( &IsPFKey($code) );
276    $result = "func-key" if ( &IsFunctionKey($code) );
277    $result = "misc-key" if ( &IsMiscFunctionKey($code) );
278    $result = "edit-key" if ( &IsEditFunctionKey($code) );
279    $result = "modifier" if ( &IsModifierKey($code) );
280    return $result;
281}
282
283sub KeyToS($$) {
284    my $code   = &codeOf( $_[0] );
285    my $state  = $_[1];
286    my $result = "";
287
288    $code = &codeOf( $Shifted{ $_[0] } ) if ( &IsShift( $_[0], $state ) );
289    my $type = &TypeOf( &toCode($code) );
290
291    if ( $type ne "other" ) {
292        $result = ( $type eq "special" ) ? "-ignore-" : "?";
293    }
294    elsif ($opt_u) {
295        $result = sprintf( "\\E[%d;%du", $code, $state );
296    }
297    else {
298        $result = sprintf( "\\E[27;%d;%d~", $state, $code );
299    }
300    return $result;
301}
302
303sub ParamToQ($) {
304    my $param  = shift;
305    my $result = shift;
306    $param--;
307    $result .= ( $param & 1 ) ? 's' : '-';
308    $result .= ( $param & 2 ) ? 'a' : '-';
309    $result .= ( $param & 4 ) ? 'c' : '-';
310    $result .= ( $param & 8 ) ? 'm' : '-';
311    return $result;
312}
313
314sub ParamToS($) {
315    my $param  = shift;
316    my $result = "";
317    if ( $param-- > 1 ) {
318        $result .= "+Shift" if ( $param & 1 );
319        $result .= "+Alt"   if ( $param & 2 );
320        $result .= "+Ctrl"  if ( $param & 4 );
321        $result .= "+Meta"  if ( $param & 8 );
322        $result =~ s/^\+//;
323    }
324    return $result;
325}
326
327sub StateToS($) {
328    my $state  = shift;
329    my $result = "";
330    $result .= "+Shift" if ( $state & $ShiftMask );
331    $result .= "+Lock"  if ( $state & $LockMask );
332    $result .= "+Ctrl"  if ( $state & $ControlMask );
333    $result .= "+Alt"   if ( $state & $AltMask );
334    $result .= "+Meta"  if ( $state & $MetaMask );
335    $result =~ s/^\+//;
336    return $result;
337}
338
339# macros/functions in xterm's input.c
340
341sub Masked($$) {
342    my $value  = shift;
343    my $mask   = shift;
344    my $result = ( ($value) & ( ~($mask) ) );
345    return $result;
346}
347
348sub IsPredefinedKey($) {
349    my $code   = &codeOf( $_[0] );
350    my $result = 0;
351    if ( $keySyms{"XK_ISO_Lock"} ) {
352        $result =
353          ( $code >= &codeOf("XK_ISO_Lock") and $code <= &codeOf("XK_Delete") )
354          ? 1
355          : 0;
356    }
357    else {
358        $result =
359          ( $code >= &codeOf("XK_BackSpace") and $code <= &codeOf("XK_Delete") )
360          ? 1
361          : 0;
362    }
363    return $result;
364}
365
366sub IsTabKey($) {
367    my $code   = &codeOf( $_[0] );
368    my $result = 0;
369    if ( $keySyms{"XK_ISO_Left_Tab"} ) {
370        $result =
371          ( $code == &codeOf("XK_Tab") || $code == &codeOf("XK_ISO_Left_Tab") );
372    }
373    else {
374        $result = ( $code == &codeOf("XK_Tab") ) ? 1 : 0;
375    }
376    return $result;
377}
378
379sub IsEditFunctionKey($) {
380    my $code   = shift;
381    my $result = 0;
382    if ( $keyCodes{$code} ) {
383        my $name = $keyCodes{$code};
384        $result = 1 if ( $editKeys{$name} );
385    }
386    return $result;
387}
388
389sub IS_CTRL($) {
390    my $code = &codeOf( $_[0] );
391    my $result = ( $code < 32 || ( $code >= 0x7f && $code <= 0x9f ) );
392    return $result;
393}
394
395sub IsControlInput($) {
396    my $code   = &codeOf( $_[0] );
397    my $result = 0;
398    $result = 1 if ( $code >= 0x40 && $code <= 0x7f );
399    return $result;
400}
401
402sub IsControlOutput($) {
403    my $code   = shift;
404    my $result = 0;
405    $result = 1 if &IS_CTRL($code);
406    return $result;
407}
408
409sub IsControlAlias($$) {
410    my $code   = shift;
411    my $state  = shift;
412    my $result = 0;
413
414    $code = &toCode($code);
415    $code = &toCode( &AliasedKey($code) );
416    if ( hex $code < 256 ) {
417        $result = &IS_CTRL($code);
418
419        # In xterm, this function does not directly test evt_state, but relies
420        # upon kd.strbuf converted by Xutf8LookupString or XmbLookupString
421        # (ultimately in _XTranslateKeysym).
422        #
423        # See https://www.mail-archive.com/xorg@lists.x.org/msg04434.html
424        #
425        # xterm does its own special cases for XK_BackSpace
426        if ( $state & $ControlMask ) {
427            my $ch = chr &codeOf($code);
428            $result = 1 if ( &IsTabKey($code) );
429            $result = 1 if ( &IsControlInput($code) );
430            $result = 1 if ( $ch =~ /^[\/ 2-8]$/ );
431        }
432    }
433    return $result;
434}
435
436sub computeMaskedModifier($$) {
437    my $state  = shift;
438    my $mask   = shift;
439    my $result = &xtermStateToParam( &Masked( $state, $mask ) );
440    return $result;
441}
442
443sub xtermStateToParam($) {
444    my $state       = shift;
445    my $modify_parm = 1;
446
447    $modify_parm += 1 if ( $state & $ShiftMask );
448    $modify_parm += 2 if ( $state & $AltMask );
449    $modify_parm += 4 if ( $state & $ControlMask );
450    $modify_parm += 8 if ( $state & $MetaMask );
451    $modify_parm = 0 if ( $modify_parm == 1 );
452    return $modify_parm;
453}
454
455sub ParamToState($) {
456    my $modify_parm = shift;
457    my $state       = 0;
458    $modify_parm-- if ( $modify_parm > 0 );
459    $state |= $ShiftMask   if ( $modify_parm & 1 );
460    $state |= $AltMask     if ( $modify_parm & 2 );
461    $state |= $ControlMask if ( $modify_parm & 4 );
462    $state |= $MetaMask    if ( $modify_parm & 8 );
463    return $state;
464}
465
466sub allowedCharModifiers($$) {
467    my $other_key = shift;
468    my $state     = shift;
469    my $code      = shift;
470    my $result = $state & ( $ShiftMask | $AltMask | $ControlMask | $MetaMask );
471
472    # If modifyOtherKeys is off or medium (0 or 1), moderate its effects by
473    # excluding the common cases for modifiers.
474    if ( $other_key <= 1 ) {
475        my $sym = $keyCodes{$code};
476        if (    &IsControlInput($code)
477            and &Masked( $result, $ControlMask ) == 0 )
478        {
479            # These keys are already associated with the control-key
480            if ( $other_key == 0 ) {
481                $result &= ~$ControlMask;
482            }
483        }
484        elsif ( $sym eq "XK_Tab" || $sym eq "XK_Return" ) {
485            #
486        }
487        elsif ( &IsControlAlias( $code, $state ) ) {
488
489            # Things like "^_" work here...
490            if ( &Masked( $result, ( $ControlMask | $ShiftMask ) ) == 0 ) {
491                if ( $sym =~ /^XK_[34578]$/ or $sym eq "XK_slash" ) {
492                    $result = 0 if ( $state == $ControlMask );
493                }
494                else {
495                    $result = 0;
496                }
497            }
498        }
499        elsif ( !&IsControlOutput($code) && !&IsPredefinedKey($code) ) {
500
501            # Printable keys are already associated with the shift-key
502            if ( !( $result & $ControlMask ) ) {
503                $result &= ~$ShiftMask;
504            }
505        }
506
507        # TODO:
508        #       result = filterAltMeta(result,
509        #                              xw->work.meta_mods,
510        #                              TScreenOf(xw)->meta_sends_esc, kd);
511        #       if (TScreenOf(xw)->alt_is_not_meta) {
512        #           result = filterAltMeta(result,
513        #                                  xw->work.alt_mods,
514        #                                  TScreenOf(xw)->alt_sends_esc, kd);
515        #       }
516    }
517    return $result;
518}
519
520# Some details are omitted (e.g., the backspace/delete toggle), but this gives
521# the general sense of the corresponding function in xterm's input.c
522sub ModifyOtherKeys($$$$) {
523    my $code        = shift;    # the keycode to test
524    my $other_key   = shift;    # "modifyOtherKeys" resource
525    my $modify_parm = shift;    # 0=unmodified, 2=shift, etc
526    my $state       = shift;    # mask of modifiers, e.g., ControlMask
527    my $result      = 0;
528
529    $modify_parm = 0 if ( $modify_parm == 1 );
530
531    if ( &IsModifierKey($code) ) {
532
533        # xterm filters out bare modifiers (ignore)
534    }
535    elsif (&IsFunctionKey($code)
536        or &IsEditFunctionKey($code)
537        or &IsKeypadKey($code)
538        or &IsCursorKey($code)
539        or &IsPFKey($code)
540        or &IsMiscFunctionKey($code)
541        or &IsPrivateKeypadKey($code) )
542    {
543        # Exclude the keys already covered by a modifier.
544    }
545    elsif ( $state > 0 ) {
546        my $sym = "";
547        $sym = $keyCodes{$code} if ( $keyCodes{$code} );
548
549        # TODO:
550        #if (IsBackarrowToggle(keyboard, kd->keysym, state)) {
551        #    kd->keysym = XK_Delete;
552        #    UIntClr(state, ControlMask);
553        #}
554        if ( !&IsPredefinedKey($code) ) {
555            $state = &allowedCharModifiers( $other_key, $state, $code );
556        }
557        if ( $state != 0 ) {
558            if ( $other_key == 1 ) {
559                if (   $sym eq "XK_BackSpace"
560                    or $sym eq "XK_Delete" )
561                {
562                }
563                elsif ( $sym eq "XK_ISO_Left_Tab" ) {
564                    $result = 1
565                      if ( &computeMaskedModifier( $state, $ShiftMask ) );
566                }
567                elsif ($sym eq "XK_Return"
568                    or $sym eq "XK_Tab" )
569                {
570                    $result = ( $modify_parm != 0 );
571                }
572                else {
573                    if ( &IsControlInput($code) ) {
574                        if ( $state == $ControlMask or $state == $ShiftMask ) {
575                            $result = 0;
576                        }
577                        else {
578                            $result = ( $modify_parm != 0 );
579                        }
580                    }
581                    elsif ( &IsControlAlias( $code, $state ) ) {
582                        if ( $state == $ShiftMask ) {
583                            $result = 0;
584                        }
585                        elsif ( &computeMaskedModifier( $state, $ControlMask ) )
586                        {
587                            $result = 1;
588                        }
589                    }
590                    else {
591                        $result = 1;
592                    }
593                }
594                if ($result) {    # second case in xterm's Input()
595                    $result = 0
596                      if ( &allowedCharModifiers( $other_key, $state, $code ) ==
597                        0 );
598                }
599            }
600            elsif ( $other_key == 2 ) {
601                if ( $sym eq "XK_BackSpace" ) {
602
603                    # strip ControlMask as per IsBackarrowToggle()
604                    $result = 1
605                      if ( &computeMaskedModifier( $state, $ControlMask ) );
606                }
607                elsif ( $sym eq "XK_Delete" ) {
608
609                    $result = ( &xtermStateToParam($state) != 0 );
610                }
611                elsif ( $sym eq "XK_ISO_Left_Tab" ) {
612                    $result = 1
613                      if ( &computeMaskedModifier( $state, $ShiftMask ) );
614                }
615                elsif ($sym eq "XK_Escape"
616                    or $sym eq "XK_Return"
617                    or $sym eq "XK_Tab" )
618                {
619
620                    $result = ( $modify_parm != 0 );
621                }
622                else {
623                    if ( &IsControlInput($code) ) {
624                        $result = 1;
625                    }
626                    elsif ( $state == $ShiftMask and $sym eq "XK_space" ) {
627                        $result = 1;
628                    }
629                    elsif ( &computeMaskedModifier( $state, $ShiftMask ) ) {
630                        $result = 1;
631                    }
632                }
633            }
634        }
635    }
636    return $result;
637}
638
639# See IsControlAlias. This handles some of the special cases where the keycode
640# seen or used by xterm is not the same as the actual keycode.
641sub AliasedKey($) {
642    my $code   = &toCode( $_[0] );
643    my $result = &codeOf($code);
644    my $sym    = $keyCodes{$code};
645    if ($sym) {
646        $result = 8  if ( $sym eq "XK_BackSpace" );
647        $result = 9  if ( $sym eq "XK_Tab" );
648        $result = 13 if ( $sym eq "XK_Return" );
649        $result = 27 if ( $sym eq "XK_Escape" );
650    }
651    return $result;
652}
653
654# Returns a short display for shift/control/alt modifiers applied to the
655# keycode to show which are affected by "modifyOtherKeys" at the given level in
656# $other_key
657sub CheckOtherKey($$) {
658    my $code      = shift;
659    my $other_key = shift;
660    my $modified  = 0;
661    my $result    = "";
662    for my $modify_parm ( 1 .. $MAXMODS ) {
663        my $state = &ParamToState($modify_parm);
664        if ( &ModifyOtherKeys( $code, $other_key, $modify_parm, $state ) ) {
665            $modified++;
666            $result .= "*";
667        }
668        else {
669            $result .= "-";
670        }
671    }
672    return $modified ? $result : "-(skip)-";
673}
674
675# Use the return-string from CheckOtherKeys as a template for deciding which
676# keys to render as escape-sequences.
677sub ShowOtherKeys($$$) {
678    my $code = &AliasedKey( $_[0] );
679    my $mode = $_[1];                  # modifyOtherKeys: 0, 1 or 2
680    my $show = $_[2];
681    my $type = &TypeOf( $_[0] );
682    my @result;
683
684    # index for $show[] can be tested with a bit-mask:
685    # 1 = shift
686    # 2 = alt
687    # 4 = ctrl
688    # 8 = meta
689    for my $c ( 0 .. length($show) - 1 ) {
690        my $rc = substr( $show, $c, 1 );
691        if ( $rc eq "*" ) {
692            $result[$c] = &KeyToS( &toCode($code), $c + 1 );
693        }
694        elsif ( $type eq "other" or ( $type eq "special" and $code < 256 ) ) {
695            my $map   = $code;
696            my $tmp   = &toCode($code);
697            my $chr   = chr hex $tmp;
698            my $shift = ( $c & 1 );
699            my $cntrl = ( $c & 4 );
700
701            # TODO - can this be simplified using xkb groups?
702            if ( $chr =~ /^[`345678]$/ and ( $c & 4 ) != 0 ) {
703                if ($shift) {
704                    $map = 30      if ( $chr eq "`" );
705                    $map = ord "#" if ( $chr eq "3" );
706                    $map = ord '$' if ( $chr eq "4" );
707                    $map = ord "%" if ( $chr eq "5" );
708                    $map = 30      if ( $chr eq "6" );
709                    $map = ord "&" if ( $chr eq "7" );
710                    $map = ord "*" if ( $chr eq "8" );
711                }
712                else {
713                    $map = 0   if ( $chr eq "`" );
714                    $map = 27  if ( $chr eq "3" );
715                    $map = 28  if ( $chr eq "4" );
716                    $map = 29  if ( $chr eq "5" );
717                    $map = 30  if ( $chr eq "6" );
718                    $map = 31  if ( $chr eq "7" );
719                    $map = 127 if ( $chr eq "8" );
720                }
721            }
722            else {
723                $map = &codeOf( $Shifted{$tmp} )
724                  if ( defined( $Shifted{$tmp} ) and $shift );
725                if ($cntrl) {
726                    if ( $chr =~ /^[190:<=>.,+*()'&%\$#"!]$/ ) {
727
728                        # ignore
729                    }
730                    elsif ( $chr =~ /^[2]$/ ) {
731                        $map = 0;
732                    }
733                    elsif ( $chr =~ /^[:;]$/ ) {
734                        $map = 27 if ( $mode > 0 );
735                    }
736                    elsif ( $chr eq '-' ) {
737                        $map = 31 if ($shift);
738                    }
739                    elsif ( $chr eq '/' ) {
740                        $map = $shift ? 127 : 31 if ( $mode == 0 );
741                        $map = 31 if ( not $shift and $mode == 1 );
742                    }
743                    elsif ( $chr eq '?' ) {
744                        $map = 127;
745                    }
746                    else {
747                        $map = ( $code & 0x1f ) if ( $code < 128 );
748                    }
749                }
750            }
751            $result[$c] = &VisibleChar( chr $map );
752        }
753        elsif ( $type eq "special" ) {
754            $result[$c] = "-ignore-";
755        }
756        else {
757            $result[$c] = sprintf( "%d:%s", $c + 1, $type );
758        }
759    }
760    return @result;
761}
762
763sub readfile($) {
764    my $data = shift;
765    my @data;
766    if ( open my $fp, $data ) {
767        @data = <$fp>;
768        close $fp;
769        chomp @data;
770    }
771    return @data;
772}
773
774sub readpipe($) {
775    my $cmd = shift;
776    return &readfile("$cmd 2>/dev/null |");
777}
778
779sub trim($) {
780    my $text = shift;
781    $text =~ s/^\s+//;
782    $text =~ s/\s+$//;
783    $text =~ s/\s+/ /g;
784    return $text;
785}
786
787sub html_ref($) {
788    my $header = shift;
789    my $anchor = lc &trim($header);
790    $anchor =~ s/\s/_/g;
791    return $anchor;
792}
793
794sub rightarrow() {
795    return $opt_h ? "&#8594;" : "->";
796}
797
798sub safe_html($) {
799    my $text = shift;
800    if ($opt_h) {
801        $text =~ s/\&/\&amp;/g;
802        $text =~ s/\</\&lt;/g;
803        $text =~ s/\</\&gt;/g;
804        if ( length($text) == 1 ) {
805            my $s = "";
806            for my $n ( 0 .. length($text) - 1 ) {
807                my $ch = substr( $text, $n, 1 );
808                my $ord = ord($ch);
809                $s .= sprintf( "&#%d;", $ord ) if ( $ord >= 128 );
810                $s .= $ch if ( $ord < 128 );
811            }
812            $text = $s;
813        }
814    }
815    return $text;
816}
817
818sub begin_report() {
819    if ($opt_o) {
820        open( $REPORT, '>', $opt_o ) or &failed("cannot open $opt_o");
821        select $REPORT;
822    }
823    if ($opt_h) {
824        printf <<EOF
825<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
826
827<html>
828<head>
829  <meta name="generator" content="$0">
830
831  <title>XTERM - Modified "Other" Keys ($xkb_layout-$xkb_model)</title>
832  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
833  <meta name="keywords" content="xterm, special keys">
834  <meta name="description" content="This is an example of xterm's modifyOtherKeys feature">
835</head>
836
837<body>
838EOF
839          ;
840    }
841}
842
843sub end_report() {
844    if ($opt_h) {
845        my $output = "output.html";
846        $output = $opt_o if ($opt_o);
847        printf <<EOF
848<div class="nav">
849  <ul>
850    <li class="nav-top"><a href="$output">(top)</a></li>
851EOF
852          ;
853        for my $h ( 0 .. $#headers ) {
854            printf "<li><a href=\"#%s\">%s</a></li>\n",
855              &html_ref( $headers[$h] ), $headers[$h];
856        }
857        printf <<EOF
858    </ul>
859</div>
860EOF
861          ;
862    }
863    if ($opt_o) {
864        select STDOUT;
865        close $REPORT;
866    }
867}
868
869sub begin_section($) {
870    my $header = shift;
871    $headers[ $#headers + 1 ] = $header;
872    if ($opt_h) {
873        printf "<h2><a name=\"%s\">%s</a></h2>\n", &html_ref($header), $header;
874    }
875    else {
876        printf "\n";
877        printf "%s:\n", $header;
878    }
879    printf STDERR "** %s\n", $header if ($opt_o);
880}
881
882sub begin_table() {
883    my $title = shift;
884    &begin_section($title);
885    if ($opt_h) {
886        printf "<table border=\"1\" summary=\"$title\">\n";
887    }
888}
889
890sub end_table() {
891    if ($opt_h) {
892        printf "</table>\n";
893    }
894}
895
896sub tt_cell($) {
897    my $text = shift;
898    return sprintf "<tt>%s</tt>", $text;
899}
900
901sub td_any($) {
902    my $text = shift;
903    return sprintf "<td>%s</td>", &tt_cell($text);
904}
905
906sub td_left($) {
907    my $text = shift;
908    return sprintf "<td align=\"left\">%s</td>", &tt_cell($text);
909}
910
911sub td_right($) {
912    my $text = shift;
913    return sprintf "<td align=\"right\">%s</td>", &tt_cell($text);
914}
915
916sub padded($$) {
917    my $size = shift;
918    my $text = shift;
919    $text = sprintf( "%*s",  $size, $text ) if ( $size > 0 );
920    $text = sprintf( "%-*s", $size, $text ) if ( $size < 0 );
921    $text =~ s/ /&nbsp;/g if ($opt_h);
922    return $text;
923}
924
925sub print_head() {
926    my $argc = $#_;
927    if ($opt_h) {
928        printf "<tr>";
929        for ( my $n = 0 ; $n <= $argc ; $n += 2 ) {
930            my $size = $_[$n];
931            my $text = &padded( $size, $_[ $n + 1 ] );
932            printf "<th>%s</th>", $text;
933        }
934        printf "</tr>\n";
935    }
936    else {
937        for ( my $n = 0 ; $n <= $argc ; $n += 2 ) {
938            my $size = $_[$n];
939            my $text = &padded( $size, $_[ $n + 1 ] );
940            printf "%s", $text;
941            printf " " if ( $n < $argc );
942        }
943        printf "\n";
944    }
945}
946
947sub link_data($$) {
948    my $thisis = shift;
949    my $thatis = shift;
950    my $column = shift;
951    my $symbol = shift;
952    my %result;
953    $result{THISIS} = $thisis;    # current table name
954    $result{THATIS} = $thatis;    # name of target table for link
955    $result{COLUMN} = $column;    # column counting from 0
956    $result{SYMBOL} = $symbol;
957    return \%result;
958}
959
960sub unique_link($$) {
961    my $thisis = shift;
962    my $symbol = shift;
963    my $unique = 0;
964    for my $n ( 0 .. length($symbol) - 1 ) {
965        $unique += ord substr( $symbol, $n, 1 );
966    }
967    return sprintf( "%s:%s.%x", $thisis, $symbol, $unique );
968}
969
970# print a row in the table, using pairs of lengths and strings:
971# + Right-align lengths greater than zero and pad;
972# + Left-align lengths less than zero, pad.
973# + For the special case of zero, just left align without padding.
974sub print_data() {
975    my $argc = $#_;
976    if ($opt_h) {
977        my @links = @{ $_[0] };
978        printf "<tr>";
979        my $col = 0;
980        for ( my $n = 1 ; $n <= $argc ; $n += 2 ) {
981            my $size = $_[$n];
982            my $text = &padded( $size, $_[ $n + 1 ] );
983            if ( $#links >= 0 ) {
984                for my $l ( 0 .. $#links ) {
985                    my %obj = %{ $links[$l] };    # link_data
986                    if ( $obj{COLUMN} == $col ) {
987                        my $props = "";
988                        my $value = &unique_link( $obj{THISIS}, $obj{SYMBOL} );
989
990                        # The symbol-map from xkbcomp has duplicates because
991                        # different modifier combinations can produce the same
992                        # keysym.  Since it appears that the slots that the
993                        # user would expect are filled in first, just ignoring
994                        # the duplicate works well enough.
995                        if ( not $linkUsed{$value} ) {
996                            $props .= " name=\"$value\"";
997                            $linkUsed{$value} = 1;
998                        }
999                        $value = &unique_link( $obj{THATIS}, $obj{SYMBOL} );
1000                        $props .= " href=\"#$value\"";
1001                        my $tail = $text;
1002                        $text =~ s/(\&nbsp;)+$//;
1003                        $tail = substr( $tail, length($text) );
1004                        $text =
1005                          sprintf( "<a %s>%s</a>%s", $props, $text, $tail );
1006                        last;
1007                    }
1008                }
1009            }
1010            printf "%s",
1011                ( $size > 0 )  ? &td_right($text)
1012              : ( $size == 0 ) ? &td_any($text)
1013              :                  &td_left($text);
1014            ++$col;
1015        }
1016        printf "</tr>\n";
1017    }
1018    else {
1019        for ( my $n = 1 ; $n <= $argc ; $n += 2 ) {
1020            my $size = $_[$n];
1021            my $text = &padded( $size, $_[ $n + 1 ] );
1022            printf "%s", $text;
1023            printf " " if ( $n < $argc );
1024        }
1025        printf "\n";
1026    }
1027}
1028
1029sub begin_preformatted($) {
1030    my $title = shift;
1031    &begin_section($title);
1032    printf "<pre>\n" if ($opt_h);
1033}
1034
1035sub end_preformatted() {
1036    printf "</pre>\n" if ($opt_h);
1037}
1038
1039sub do_localectl($) {
1040    my $report = shift;
1041    my $cmd    = "localectl status";
1042    my @data   = &readpipe($cmd);
1043    &begin_table("Output of $cmd") if ($report);
1044    for my $n ( 0 .. $#data ) {
1045
1046        # let command-line parameters override localectl output, for reports
1047        $data[$n] =~ s/^(\s+X11 Layout:\s+).*$/$1$opt_l/ if ($opt_l);
1048        $data[$n] =~ s/^(\s+X11 Model:\s+).*$/$1$opt_m/  if ($opt_m);
1049        my @fields = split /:\s*/, $data[$n];
1050        next unless ( $#fields == 1 );
1051        if ($report) {
1052            if ($opt_h) {
1053                printf "<tr>%s%s</tr>\n",
1054                  &td_right( $fields[0] ),
1055                  &td_left( $fields[1] );
1056            }
1057            else {
1058                printf "%s\n", $data[$n];
1059            }
1060        }
1061        $xkb_layout = $fields[1] if ( $fields[0] =~ /x11 layout/i );
1062        $xkb_model  = $fields[1] if ( $fields[0] =~ /x11 model/i );
1063    }
1064    if ($report) {
1065        &end_table;
1066    }
1067}
1068
1069sub do_keysymdef() {
1070    my @data    = &readfile($keyfile);
1071    my $lenSyms = 0;
1072    for my $n ( 0 .. $#data ) {
1073        my $value = &trim( $data[$n] );
1074        next unless ( $value =~ /^#define\s+XK_/ );
1075        my $name = $value;
1076        $name =~ s/^#define\s+//;
1077        $value = $name;
1078        $name =~ s/\s.*//;
1079        $value =~ s/^[^\s]+\s+//;
1080        my $note = $value;
1081        $value =~ s/\s.*//;
1082
1083        $note =~ s/^[^\s]+\s*//;
1084        if ( $note !~ /\b(alias|deprecated)\b/ ) {
1085
1086            if ( $note =~ /\/*.*\bU\+[[:xdigit:]]{4,8}.*\*\// ) {
1087                next if ( $note =~ /\(U\+/ );
1088                my $code = $note;
1089                $code =~ s/^.*\bU\+([[:xdigit:]]+).*/$1/;
1090                $note =~ s/^\/\*[([:space:]]*//;
1091                $note =~ s/[)[:space:]]*\*\/$//;
1092                $uniNames{$value} = $note;
1093                $uniCodes{$value} = hex $code;
1094            }
1095        }
1096        $lenSyms        = length($name) if ( length($name) > $lenSyms );
1097        $value          = lc $value;
1098        $keySyms{$name} = $value;
1099        $keyCodes{$value} = $name unless ( $keyCodes{$value} );
1100        printf "keySyms{$name} = '$value', keyCodes{$value} = $name\n"
1101          if ($opt_d);
1102    }
1103    my $tmpfile = $keyfile;
1104    $tmpfile =~ s/^.*\///;
1105    &begin_table("Symbols from $tmpfile");
1106    my @keys = keys %keySyms;
1107    &print_data( \@nolinks, 5, sprintf( "%d", $#keys ),
1108        0, sprintf( "keysyms are defined (longest %d)", $lenSyms ) );
1109    @keys = keys %keyCodes;
1110    &print_data( \@nolinks, 5, sprintf( "%d", $#keys ),
1111        0, "keycodes are defined" );
1112    @keys = keys %uniCodes;
1113    &print_data( \@nolinks, 5, sprintf( "%d", $#keys ),
1114        0, "keycodes are equated to Unicode" );
1115    &end_table;
1116}
1117
1118# For what it's worth, there is a C library (xkbfile) which could be used,
1119# but there is no documentation and would not actually solve the problem at
1120# hand.
1121#
1122# setxkbmap -model pc105 -layout us -print | xkbcomp - -C -o -
1123sub do_xkbcomp() {
1124    my @data =
1125      &readpipe( "setxkbmap "
1126          . "-model $xkb_model "
1127          . "-layout $xkb_layout -print "
1128          . "| xkbcomp - -C -o -" );
1129    my $state = -1;
1130    my $type  = {};
1131    for my $n ( 0 .. $#data ) {
1132        if ( $data[$n] =~ /static.*\bkeyNames\[.*{/ ) {
1133            $state = 0;
1134            next;
1135        }
1136        if ( $data[$n] =~ /static.*\bsymCache\[.*{/ ) {
1137            $state = 1;
1138            next;
1139        }
1140        if ( $data[$n] =~ /static.*\bsymMap\[.*{/ ) {
1141            $state = 2;
1142            next;
1143        }
1144        if ( $data[$n] =~ /static.*\bdflt_types\[.*{/ ) {
1145            $state = 3;
1146            next;
1147        }
1148        if ( $state >= 0 ) {
1149            if ( $data[$n] =~ /^\s*};/ ) {
1150                printf "# %s\n", $data[$n] if ($opt_d);
1151                $state = -1;
1152                next;
1153            }
1154            printf "* %s\n", $data[$n] if ($opt_d);
1155        }
1156
1157        # parse data in "keyNames[NUM_KEYS]"
1158        if ( $state == 0 ) {
1159            my $text = $data[$n];
1160            my $name;
1161            while ( $text =~ /^.*".*".*$/ ) {
1162                $text =~ s/^[^"]*//;
1163                $name = $text;
1164                $name =~ s/"\s+}.*//;
1165                $name =~ s/"//g;
1166                $keyNames[ $#keyNames + 1 ] = $name;
1167                printf "keyNames[%d] = '%s'\n", $#keyNames,
1168                  $keyNames[$#keyNames]
1169                  if ($opt_v);
1170                $text =~ s/^"[^"]*"//;
1171            }
1172        }
1173
1174        # parse data in "symCache[NUM_SYMBOLS]"
1175        elsif ( $state == 1 ) {
1176            my $text = $data[$n];
1177            my $name;
1178            while ( $text =~ /[[:alnum:]_]/ ) {
1179                $text =~ s/^[^[[:alnum:]_]*//;
1180                $name = $text;
1181                $name =~ s/[^[[:alnum:]_].*//;
1182                $symCache[ $#symCache + 1 ] = $name;
1183                printf "symCache[%d] = %s\n", $#symCache, $symCache[$#symCache]
1184                  if ($opt_v);
1185                $text =~ s/^[[:alnum:]_]+//;
1186            }
1187        }
1188
1189        # parse data in "symMap[NUM_KEYS]"
1190        elsif ( $state == 2 ) {
1191            my $text = $data[$n];
1192            my $code;
1193            while ( $text =~ /[{].*[}]/ ) {
1194                my %obj;
1195                $text =~ s/^[^{]*[{]\s*//;
1196                $code = $text;
1197                $code =~ s/[^[[:alnum:]].*//;
1198                $text =~ s/[[:alnum:]]+\s*,\s*//;
1199                $obj{TYPE} = $code;    # KeyType
1200                my %tmp = %{ $keyTypes[$code] };
1201                $tmp{USED} += 1;
1202                $keyTypes[$code] = \%tmp;
1203                $code = $text;
1204                $code =~ s/[^[[:alnum:]].*//;
1205                $text =~ s/[[:alnum:]]+\s*,\s*//;
1206                $obj{USED} = hex $code;    # 0/1 for used/unused
1207                $code = $text;
1208                $code =~ s/[^[[:alnum:]].*//;
1209                $obj{CODE} = $code;        # index in symCache[]
1210                $text =~ s/[[:alnum:]]+\s*//;
1211                $symMap[ $#symMap + 1 ] = \%obj;
1212                printf "symMap[%d] = {%d,%d,%d}\n", $#symMap, $obj{TYPE},
1213                  $obj{USED}, $obj{CODE}
1214                  if ($opt_v);
1215            }
1216        }
1217
1218        # parse data in "dflt_types[]"
1219        elsif ( $state == 3 ) {
1220            my $text = &trim( $data[$n] );
1221            if ( $text =~ /^\s*[}](,)?$/ ) {
1222                $type->{USED}               = 0;
1223                $keyTypes[ $#keyTypes + 1 ] = $type;
1224                $type                       = {};
1225            }
1226            elsif ( $text =~ /^\d+,$/ ) {
1227                $text =~ s/,//;
1228                $type->{SIZE} = $text;
1229            }
1230            elsif ( $text =~ /^None,\s+lnames_[[:alnum:]_]+$/ ) {
1231                $text =~ s/^None,\s+lnames_//;
1232                $type->{NAME} = $text;
1233            }
1234            elsif ( $text =~ /^\s*[{].*[}],\s*$/ ) {
1235                $text =~ s/^\s*[{]\s*([^,]+),.*/$1/;
1236                $type->{MODS} = $text;
1237            }
1238        }
1239    }
1240    &begin_table("Summary from xkbcomp");
1241    &print_data( \@nolinks, 5, sprintf( "%d", $#keyNames + 1 ), 0, "keyNames" );
1242    &print_data( \@nolinks, 5, sprintf( "%d", $#keyTypes + 1 ), 0, "keyTypes" );
1243    &print_data( \@nolinks, 5, sprintf( "%d", $#symCache + 1 ), 0, "symCache" );
1244    &print_data( \@nolinks, 5, sprintf( "%d", $#symMap + 1 ),   0, "symMap" );
1245    &end_table;
1246}
1247
1248# Report keysymdef.h without the deprecated stuff, and sorted by keycode.
1249sub report_keysymdef() {
1250    &begin_table("Key symbols");
1251    &print_head( 0, "Code", 0, "Category", 0, "Symbol" );
1252
1253    # sort by numeric keycode rather than string
1254    my @keyCodes = keys %keyCodes;
1255    my @sortCodes;
1256    for my $c ( 0 .. $#keyCodes ) {
1257        $sortCodes[$c] = sprintf "%08X", hex $keyCodes[$c];
1258    }
1259    @sortCodes = sort @sortCodes;
1260    for my $c ( 0 .. $#sortCodes ) {
1261        my $code = sprintf( "0x%04x", hex $sortCodes[$c] );
1262        my $sym = $keyCodes{$code};
1263        &print_data( \@nolinks, 9, $code, -8, &TypeOf($code), 0, $sym );
1264    }
1265    &end_table;
1266}
1267
1268sub report_key_types() {
1269    &begin_table("Key types");
1270    &print_head( 5, "Type", 5, "Used", 5, "Levels", 0, "Name" );
1271    for my $t ( 0 .. $#keyTypes ) {
1272        my %type = %{ $keyTypes[$t] };
1273        next if ( $type{USED} == 0 and not $opt_v );
1274        &print_data(
1275            \@nolinks, 5, sprintf( "%d", $t ), 5,
1276            sprintf( "%d", $type{USED} ), 5, sprintf( "%d", $type{SIZE} ), 0,
1277            $type{NAME}
1278        );
1279    }
1280    &end_table;
1281}
1282
1283sub report_modified_keys() {
1284    my @codes = sort keys %keysUsed;
1285    my $width = 14;
1286    &begin_table("Other modifiable keycodes");
1287    &print_head(
1288        0,       "Code",   0,       "Symbol", 0,       "Actual",
1289        -$width, "Mode 0", -$width, "Mode 1", -$width, "Mode 2"
1290    );
1291    $width = 0 if ($opt_h);
1292    for my $c ( 0 .. $#codes ) {
1293        next unless ( $codes[$c] ne "" );
1294        my @links;
1295        my $sym = $keysUsed{ $codes[$c] };
1296        $links[0] = &link_data( "summary", "detailed", 1, $sym );
1297        &print_data(
1298            \@links,
1299            6,   $codes[$c],                         #
1300            -20, $keysUsed{ $codes[$c] },            #
1301            -6,  sprintf( "%d", hex $codes[$c] ),    #
1302            -$width, &CheckOtherKey( $codes[$c], 0 ),    #
1303            -$width, &CheckOtherKey( $codes[$c], 1 ),    #
1304            -$width, &CheckOtherKey( $codes[$c], 2 )
1305        );
1306    }
1307    &end_table;
1308    &begin_preformatted("Modify-param to/from state");
1309    for my $param ( 0 .. $MAXMODS ) {
1310        my $state = &ParamToState($param);
1311        my $check = &xtermStateToParam($state);
1312        printf " PARAM %d %s %d %s %d (%s)\n", $param, &rightarrow,    #
1313          $state, &rightarrow,                                         #
1314          $check, &ParamToS($param);
1315    }
1316    &end_preformatted;
1317    &begin_preformatted("State to/from modify-param");
1318    for my $state ( 0 .. 15 ) {
1319        my $param = &xtermStateToParam($state);
1320        my $check = &ParamToState($param);
1321        printf " STATE %d %s %d %s %d (%s)\n",                         #
1322          $state, &rightarrow,                                         #
1323          $param, &rightarrow,                                         #
1324          $check, &StateToS($state);
1325    }
1326    &end_preformatted;
1327}
1328
1329# Make a report showing user- and program-modes.
1330sub report_otherkey_escapes() {
1331    my @codes = sort keys %keysUsed;
1332    my $width = 14;
1333    &begin_table("Other modified-key escapes");
1334    &print_head(
1335        0,       "Code",   0,       "Symbol", 0,       "Actual",
1336        -$width, "Mode 0", -$width, "Mode 1", -$width, "Mode 2"
1337    );
1338    $width = 0 if ($opt_h);
1339    for my $c ( 0 .. $#codes ) {
1340        next unless ( $codes[$c] ne "" );
1341        my $level0 = &CheckOtherKey( $codes[$c], 0 );
1342        my $level1 = &CheckOtherKey( $codes[$c], 1 );
1343        my $level2 = &CheckOtherKey( $codes[$c], 2 );
1344        my @level0 = &ShowOtherKeys( $codes[$c], 0, $level0 );
1345        my @level1 = &ShowOtherKeys( $codes[$c], 1, $level1 );
1346        my @level2 = &ShowOtherKeys( $codes[$c], 2, $level2 );
1347        my @links;
1348        my $sym = $keysUsed{ $codes[$c] };
1349        $links[0] = &link_data( "detailed", "symmap", 1, $sym );
1350        &print_data(
1351            \@links,    #
1352            -6,  $codes[$c],                         #
1353            -20, $keysUsed{ $codes[$c] },            #
1354            -6,  sprintf( "%d", hex $codes[$c] ),    #
1355            -$width, $level0,                        #
1356            -$width, $level1,                        #
1357            -$width, $level2
1358        );
1359
1360        for my $r ( 0 .. $#level0 ) {
1361            &print_data(
1362                \@nolinks,                           #
1363                -6,  &ParamToQ( $r + 1 ),            #
1364                -20, "",                             #
1365                -6,  "",                             #
1366                -$width, &safe_html( $level0[$r] ),  #
1367                -$width, &safe_html( $level1[$r] ),  #
1368                -$width, &safe_html( $level2[$r] )
1369            );
1370        }
1371    }
1372    &end_table;
1373}
1374
1375sub report_keys_used() {
1376    &begin_table("Key map");
1377    &print_head(
1378        5, "Type",                                   #
1379        0, "Level",                                  #
1380        0, "Name",                                   #
1381        6, "Code",                                   #
1382        0,
1383        "Symbol"
1384    );
1385    for my $m ( 0 .. $#symMap ) {
1386        my %obj = %{ $symMap[$m] };
1387        next unless ( $obj{USED} );
1388        my $sym = $symCache[ $obj{CODE} ];
1389        next if ( $sym eq "NoSymbol" );
1390        my $code = "";
1391        $code = $keySyms{$sym} if ( $keySyms{$sym} );
1392        next if ( $code eq "" );
1393        $keysUsed{$code} = $sym;
1394        my %type = %{ $keyTypes[ $obj{TYPE} ] };
1395        my @links;
1396        $links[0] = &link_data( "symmap", "summary", 4, $sym );
1397        &print_data(
1398            \@links,
1399            5, sprintf( "%d",   $obj{TYPE} ),     #
1400            5, sprintf( "1/%d", $type{SIZE} ),    #
1401            -4, $keyNames[$m],                    #
1402            6,  $code,                            #
1403            0,  $sym
1404        );
1405
1406        my $base = $code;
1407        $Shifted{$code} = $code unless ( $Shifted{$code} );
1408
1409        for my $t ( 1 .. $type{SIZE} - 1 ) {
1410            $sym = $symCache[ $obj{CODE} + $t ];
1411            if ( $keySyms{$sym} ) {
1412                $code = $keySyms{$sym};
1413                $keysUsed{$code} = $sym;
1414                $links[0] = &link_data( "symmap", "summary", 4, $sym );
1415            }
1416            else {
1417                $code  = "";
1418                @links = ();
1419            }
1420            &print_data(
1421                \@links,
1422                5,  "",                                         #
1423                5,  sprintf( "%d/%d", $t + 1, $type{SIZE} ),    #
1424                -4, "",                                         #
1425                6,  $code,                                      #
1426                0,  $sym
1427            );
1428            @links = ();
1429
1430            # The shift-modifier could be used in custom groups, but the only
1431            # built-in ones that appear relevant are TWO_LEVEL and ALPHABETIC,
1432            # which have two levels.  This records the shifted code for a given
1433            # base.
1434            if (    $type{SIZE} == 2
1435                and $type{MODS}
1436                and index( $type{MODS}, "ShiftMask" ) >= 0 )
1437            {
1438                if ( $t == 1 ) {
1439                    $Shifted{$base} = $code;
1440                }
1441                elsif ( not $Shifted{$code} ) {
1442                    $Shifted{$code} = $code;
1443                }
1444            }
1445        }
1446    }
1447    &end_table;
1448}
1449
1450sub KeyClasses($) {
1451    my $hex   = shift;
1452    my $alias = &IsControlAlias( $hex, $ControlMask ) ? "alias" : "";
1453    my $cntrl = &IS_CTRL($hex) ? "cntrl" : "";
1454    my $ctl_i = &IsControlInput($hex) ? "ctl_i" : "";
1455    my $ctl_o = &IsControlOutput($hex) ? "ctl_o" : "";
1456    my $this  = sprintf( "%-5s %-5s %-5s %-5s %-8s",
1457        $alias, $cntrl, $ctl_i, $ctl_o, &TypeOf($hex) );
1458}
1459
1460sub report_key_classes() {
1461    &begin_table("Keycode-classes");
1462    my $base = -1;
1463    my $last = "";
1464    my $next = 65535;
1465    my $form = " [%8s .. %-8s] %s\n";
1466    &print_head( 0, "First", 0, "Last", 0, "Classes" ) if ($opt_h);
1467    for my $code ( 0 .. $next ) {
1468        my $hex  = &toCode($code);
1469        my $this = &KeyClasses($hex);
1470        if ( $base < 0 ) {
1471            $base = 0;
1472            $last = $this;
1473        }
1474        elsif ( $this ne $last ) {
1475            printf $form, &toCode($base), &toCode( $code - 1 ), $last
1476              unless ($opt_h);
1477            &print_data( \@nolinks, 0, &toCode($base), 0, &toCode( $code - 1 ),
1478                0, $last )
1479              if ($opt_h);
1480            $base = $code;
1481            $last = $this;
1482        }
1483    }
1484    printf $form, &toCode($base), &toCode($next), $last unless ($opt_h);
1485    &print_data( \@nolinks, 0, &toCode($base), 0, &toCode($next), 0, $last )
1486      if ($opt_h);
1487    &end_table;
1488}
1489
1490sub main::HELP_MESSAGE() {
1491    printf STDERR <<EOF
1492Usage: $0 [options]
1493
1494Options:
1495  -d      debug
1496  -h      write report with html-markup
1497  -k      dump keysyms/keycodes from $keyfile
1498  -K      dump keycode-classes
1499  -l XXX  use XXX for Xkb layout (default $xkb_layout)
1500  -m XXX  use XXX for Xkb model (default $xkb_model)
1501  -o XXX  write report to the file XXX.
1502  -u      use CSI u format for escapes
1503  -v      verbose
1504
1505EOF
1506      ;
1507    exit 1;
1508}
1509
1510binmode( STDOUT, ":utf8" );
1511
1512&do_localectl(0);
1513
1514$Getopt::Std::STANDARD_HELP_VERSION = 1;
1515&getopts('dhKkl:m:o:uv') || &main::HELP_MESSAGE;
1516$opt_v = 1 if ($opt_d);
1517
1518&begin_report;
1519
1520&do_localectl(1);
1521
1522$xkb_layout = $opt_l if ($opt_l);
1523$xkb_model  = $opt_m if ($opt_m);
1524
1525&do_keysymdef;
1526&report_keysymdef if ($opt_k);
1527
1528&do_xkbcomp;
1529
1530&report_key_classes if ($opt_K);
1531
1532&report_key_types;
1533&report_keys_used;
1534&report_modified_keys;
1535&report_otherkey_escapes;
1536
1537&end_report;
1538
15391;
1540