titlestack.pl revision a5ae21e4
1#!/usr/bin/env perl
2# $XTermId: titlestack.pl,v 1.29 2019/09/20 00:50:10 tom Exp $
3# -----------------------------------------------------------------------------
4# this file is part of xterm
5#
6# Copyright 2019 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# Test the title-stack and title-mode options of xterm.
35
36# TODO: add test for arbitrary x property
37# TODO: allow -g and -v options to toggle interactively
38
39use strict;
40use warnings;
41
42use Getopt::Std;
43use Encode qw(decode encode);
44use Term::ReadKey;
45use I18N::Langinfo qw(langinfo CODESET);
46
47our $target = "";
48
49our $encoding = lc( langinfo( CODESET() ) );
50our $wm_name;
51our ( $opt_b, $opt_g, $opt_v, $opt_8 );
52
53our @titlestack;    # stack of title-strings, using current encoding
54our @item_stack;    # selector used when doing a push
55our @mode_stack;    # titleModes in effect when titlestack was loaded
56our $SP;            # stack-pointer
57our $TM;            # current titleModes, in various combinations
58
59our $utf8_sample = 0;
60
61our $CSI = "\x1b[";
62our $OSC = "\x1b]";
63our $ST  = "\x1b\\";
64
65sub SendHEX()  { return ( $TM & 1 ) ? 1 : 0; }
66sub ReadHEX()  { return ( $TM & 2 ) ? 1 : 0; }
67sub SendUTF8() { return ( $TM & 4 ) ? 1 : 0; }
68sub ReadUTF8() { return ( $TM & 8 ) ? 1 : 0; }
69
70sub to_hex($) {
71    my $value  = shift;
72    my $result = "";
73    my $n;
74
75    for ( $n = 0 ; $n < length($value) ; ++$n ) {
76        $result .= sprintf( "%02X", ord substr( $value, $n, 1 ) );
77    }
78    return $result;
79}
80
81sub from_hex($) {
82    my $value  = shift;
83    my $result = "";
84    if ( $value =~ /^[[:xdigit:]]+$/ and ( length($value) % 2 ) == 0 ) {
85        my $octets = "";
86        for ( my $n = 0 ; $n < length($value) ; $n += 2 ) {
87            my $pair = substr( $value, $n, 2 );
88            my $data = hex $pair;
89            $octets .= chr($data);
90        }
91        $result = decode( &ReadUTF8 ? "utf-8" : "iso-8859-1", $octets );
92    }
93    else {
94        $result = $value;
95    }
96    return $result;
97}
98
99sub show_string($) {
100    my $value = shift;
101    my $n;
102    my $octets =
103      encode( ( ( $encoding eq "utf-8" ) ? "utf-8" : "iso-8859-1" ), $value );
104
105    my $result = "";
106    for ( $n = 0 ; $n < length($octets) ; $n += 1 ) {
107        my $c = ord substr( $octets, $n, 1 );
108        if ( $c == ord '\\' ) {
109            $result .= "\\\\";
110        }
111        elsif ( $c == 0x1b ) {
112            $result .= "\\E";
113        }
114        elsif ( $c == 0x7f ) {
115            $result .= "^?";
116        }
117        elsif ( $c == 32 ) {
118            $result .= "\\s";
119        }
120        elsif ( $c < 32 ) {
121            $result .= sprintf( "^%c", $c + 64 );
122        }
123        elsif ( $c > 128 ) {
124            $result .= sprintf( "\\%03o", $c );
125        }
126        else {
127            $result .= chr($c);
128        }
129    }
130
131    printf "%s\r\n", $result;
132}
133
134sub send_command($) {
135    my $command = shift;
136    if ($opt_v) {
137        printf "send: ";
138        &show_string($command);
139    }
140    print STDERR encode( &SendUTF8 ? "utf-8" : "iso-8859-1", $command );
141}
142
143sub get_reply($) {
144    my $command = shift;
145    my $reply   = "";
146
147    &send_command($command);
148    my $start = time;
149    while (1) {
150        my $test = ReadKey 1;
151        last if not defined $test;
152        last if ( time > ( $start + 1 ) );
153
154        $reply .= $test;
155    }
156    if ($opt_v) {
157        printf "read: ";
158        &show_string($reply);
159    }
160    return $reply;
161}
162
163sub get_title($) {
164    my $icon   = shift;
165    my $reply  = &get_reply( sprintf( "%s%dt", $CSI, $icon ? 20 : 21 ) );
166    my $prefix = $icon ? "L" : "l";
167
168    if ( $opt_8 and ( $reply =~ /^$CSI/ ) ) {
169        $reply =~ s/^${CSI}//;
170        $reply =~ s/${ST}$//;
171    }
172    else {
173        $reply =~ s/^\x1b//;
174        $reply =~ s/^[\[\]]//;
175        if ( index( $reply, $ST ) >= 0 ) {
176            $reply =~ s/\x1b\\$//;
177        }
178        else {
179            $reply =~ s/\007$//;
180        }
181    }
182    if ( $reply =~ /^$prefix/ ) {
183        $reply =~ s/^$prefix//;
184        if (&ReadHEX) {
185            $reply = &from_hex($reply);
186        }
187    }
188    else {
189        $reply = "?" . $reply;
190    }
191    return $reply;
192}
193
194sub raw() {
195    ReadMode 'ultra-raw', 'STDIN';    # allow single-character inputs
196}
197
198sub cooked() {
199    ReadMode 'normal';
200}
201
202sub read_cmd($) {
203    my $command = shift;
204    my @result;
205    if ( open( my $fp, "$command |" ) ) {
206        binmode( $fp, ":utf8" ) if ( $encoding eq "utf-8" );
207        @result = <$fp>;
208        close($fp);
209        chomp @result;
210    }
211    return @result;
212}
213
214sub which_modes($) {
215    my $modes  = shift;
216    my $result = "";
217    if ( $modes & 3 ) {
218        $result .= "put" if ( ( $modes & 3 ) == 1 );
219        $result .= "get" if ( ( $modes & 3 ) == 2 );
220        $result .= "p/q" if ( ( $modes & 3 ) == 3 );
221        $result .= " hex";
222    }
223    if ( $modes & 12 ) {
224        $modes /= 4;
225        $result .= "," unless ( $result eq "" );
226        $result .= "put" if ( ( $modes & 3 ) == 1 );
227        $result .= "get" if ( ( $modes & 3 ) == 2 );
228        $result .= "p/q" if ( ( $modes & 3 ) == 3 );
229        $result .= " utf";
230    }
231    $result = "default" if ( $result eq "" );
232    return $result;
233}
234
235sub which_tmode($$) {
236    my $set    = shift;
237    my $mode   = shift;
238    my $result = "";
239    $result = "set window/icon labels using hexadecimal"   if ( $mode == 0 );
240    $result = "query window/icon labels using hexadecimal" if ( $mode == 1 );
241    $result = "set window/icon labels using UTF-8"         if ( $mode == 2 );
242    $result = "query window/icon labels using UTF-8"       if ( $mode == 3 );
243    $result = "do not " . $result if ( $set == 0 and $result ne "" );
244    return $result;
245}
246
247sub get_tmode($) {
248    my $set    = shift;
249    my $help   = 0;
250    my $result = "?";
251    while ( $result !~ /^[0123]$/ ) {
252        $result = ReadKey 0;
253        if ( $result eq "q" ) {
254            $result = -1;
255            last;
256        }
257        elsif ( $result eq "?" and not $help ) {
258            for my $n ( 0 .. 3 ) {
259                printf "\r\n\t%s = %s", $n, &which_tmode( $set, $n );
260            }
261            printf "\r\n\t:";
262            $help = 1;
263        }
264    }
265    if ( $result >= 0 ) {
266        printf "[%s]\r\n\t:", &which_tmode( $set, $result );
267    }
268    return $result;
269}
270
271sub which_item($) {
272    my $code   = shift;
273    my $result = "";
274    $result = "both" if ( $code == 0 );
275    $result = "icon" if ( $code == 1 );
276    $result = "name" if ( $code == 2 );
277    return $result;
278}
279
280sub which_selector($) {
281    my $code   = shift;
282    my $result = "";
283    $result = "both titles"  if ( $code == 0 );
284    $result = "icon title"   if ( $code == 1 );
285    $result = "window title" if ( $code == 2 );
286    return $result;
287}
288
289sub get_selector() {
290    my $result = "?";
291    my $help   = 0;
292    printf "\t:";
293    while ( $result !~ /^[012]$/ ) {
294        $result = ReadKey 0;
295        if ( $result eq "q" ) {
296            $result = -1;
297            last;
298        }
299        elsif ( $result eq "l" ) {
300            $result = 2;
301        }
302        elsif ( $result eq "L" ) {
303            $result = 1;
304        }
305        elsif ( $result eq "?" and not $help ) {
306            for my $n ( 0 .. 2 ) {
307                printf "\r\n\t%d = %s", $n, &which_selector($n);
308            }
309            printf "\r\n\t:";
310            $help = 1;
311        }
312    }
313    if ( $result >= 0 ) {
314        printf "[%s]\r\n\t:", &which_selector($result);
315    }
316    return $result;
317}
318
319sub display_info() {
320
321    # use xprop to get properties
322    my $command = "xprop";
323    if ( $ENV{WINDOWID} ) {
324        my $windowid = $ENV{WINDOWID};
325        $command .= " -id " . $windowid if ( $windowid ne "" );
326    }
327    else {
328        printf "...xprop\r\n";
329    }
330    my @props = &read_cmd($command);
331    for my $n ( 0 .. $#props ) {
332        printf "\t%s\r\n", $props[$n]
333          if ( index( $props[$n], "WM_NAME(" ) >= 0
334            or index( $props[$n], "WM_ICON_NAME(" ) >= 0 );
335    }
336
337    # use escape sequences to get corresponding information
338    printf "... Icon title:%s\r\n",   &get_title(1);
339    printf "... Window title:%s\r\n", &get_title(0);
340
341    # show title-stack (and modes used for each level)
342    printf "... Modes[%s]\r\n",  &which_modes($TM);
343    printf "... Stack(%d):\r\n", $SP;
344    for my $n ( 0 .. $SP ) {
345        printf "\t%d [%s:%s]%s\r\n", $n, &which_item( $item_stack[$n] ),
346          &which_modes( $mode_stack[$n] ), $titlestack[$n];
347    }
348}
349
350sub set_titlemode($) {
351    my $set  = shift;
352    my $opts = "";
353    my $opt;
354    printf "\t:";
355    while ( ( $opt = &get_tmode($set) ) >= 0 ) {
356        $TM |= ( 1 << $opt ) if ($set);
357        $TM &= ~( 1 << $opt ) unless ($set);
358        $opts .= ";" unless ( $opts eq "" );
359        $opts .= $opt;
360    }
361    if ( $opts ne "" ) {
362        &send_command( sprintf( "%s>%s%s", $CSI, $opts, $set ? "t" : "T" ) );
363    }
364}
365
366sub utf8_sample($) {
367    my $item = shift;
368    my $last = 4;
369    my $text;
370    if ( ( $item % $last ) == 0 ) {
371        my $chars = "THE QUICK BROWN FOX\nJUMPED OVER THE LAZY DOG";
372        $text = "";
373        for my $n ( 0 .. length($chars) ) {
374            my $chr = substr( $chars, $n, 1 );
375            if ( $chr eq " " ) {
376                $chr = "  ";
377            }
378            elsif ( ord($chr) < 32 ) {
379
380                # leave control characters as-is
381            }
382            else {
383                $chr = chr( 0xff00 + ord($chr) - 32 );
384            }
385            $text .= $chr;
386        }
387    }
388    elsif ( ( $item % $last ) == 1 ) {
389        $text = chr(0x442) . chr(0x435) . chr(0x441) . chr(0x442);
390    }
391    elsif ( ( $item % $last ) == 2 ) {
392        for my $chr ( 0x391 .. 0x3a9 ) {
393            $text .= chr($chr);
394        }
395    }
396    elsif ( ( $item % $last ) == 3 ) {
397        for my $chr ( 0x3b1 .. 0x3c9 ) {
398            $text .= chr($chr);
399        }
400    }
401    return $text;
402}
403
404sub set_titletext() {
405    my $opt = &get_selector;
406    if ( $opt >= 0 ) {
407        my $text;
408        if ($opt_g) {
409
410            if (&SendUTF8) {
411                $text = &utf8_sample( $utf8_sample++ );
412            }
413            else {
414                # ugly code, but mapping the a/e/i/o/u uppercase accented
415                # characters that repeat.
416                my $a_chars = chr(192) . chr(193) . chr(194) . chr(196);
417                my $e_chars = "";
418                my $i_chars = " ";
419                my $o_chars = chr(210) . chr(211) . chr(212) . chr(214);
420                my $u_chars = "";
421                my $gap     = " " . chr(215) . " ";
422                for my $chr ( 0 .. 3 ) {
423                    $e_chars .= chr( $chr + 200 );
424                    $i_chars .= chr( $chr + 204 ) . " ";
425                    $u_chars .= chr( $chr + 217 );
426                }
427                $text =
428                    $a_chars
429                  . $gap
430                  . $e_chars
431                  . $gap
432                  . $i_chars
433                  . $gap
434                  . $o_chars
435                  . $gap
436                  . $u_chars;
437            }
438            printf "%s\r\n", $text;
439        }
440        else {
441            &cooked;
442            $text = ReadLine 0;
443            chomp $text;
444            &raw;
445        }
446        $titlestack[$SP] = $text;
447        $item_stack[$SP] = $opt;
448        $mode_stack[$SP] = $TM;
449        if (&SendHEX) {
450            my $octets =
451              encode( ( &SendUTF8 ? "utf-8" : "iso-8859-1" ), $text );
452            $text = &to_hex($octets);
453        }
454        &send_command( sprintf( "%s%s;%s%s", $OSC, $opt, $text, $ST ) );
455    }
456}
457
458sub save_title() {
459    my $opt = &get_selector;
460    if ( $opt >= 0 ) {
461        &send_command( sprintf( "%s22;%st", $CSI, $opt ) );
462        ++$SP;
463        $titlestack[$SP] = $titlestack[ $SP - 1 ];
464        $item_stack[$SP] = $opt;
465        $mode_stack[$SP] = $mode_stack[ $SP - 1 ];
466    }
467}
468
469sub restore_title($) {
470    my $set = shift;
471    my $opt = &get_selector unless ($set);
472    if ( $opt >= 0 and $SP > 0 ) {
473        $opt = $item_stack[$SP] if ($set);
474        &send_command( sprintf( "%s23;%st", $CSI, $opt ) );
475        $SP--;
476    }
477}
478
479sub get_xprop($$) {
480    my $id   = shift;
481    my $name = shift;
482    my @data = &read_cmd("xprop -id $id");
483    my $prop = "";
484    for my $n ( 0 .. $#data ) {
485        if ( $data[$n] =~ /$name\([^)]+\) =/ ) {
486            $prop = $data[$n];
487            $prop =~ s/^[^=]*=\s*//;
488            $prop =~ s/"//g;
489            last;
490        }
491    }
492    return $prop;
493}
494
495sub get_WM_NAME() {
496    $wm_name = "missing WM_NAME";
497    my $supwin = `xprop -root '_NET_SUPPORTING_WM_CHECK'`;
498    if ( $supwin ne "" ) {
499        $supwin =~ s/^.*(0x[[:xdigit:]]+).*/$1/;
500        $wm_name = &get_xprop( $supwin, "_NET_WM_NAME" );
501        $wm_name = "unknown" if ( $wm_name eq "" );
502        printf "** using \"$wm_name\" window manager\n";
503    }
504}
505
506sub main::HELP_MESSAGE() {
507    printf STDERR <<EOF
508Usage: $0 [options]
509Options:
510  -8      use 8-bit controls
511  -b      use BEL rather than ST for terminating strings
512  -g      generate title-strings rather than prompting
513  -v      verbose
514EOF
515      ;
516    exit 1;
517}
518
519$Getopt::Std::STANDARD_HELP_VERSION = 1;
520&getopts('bgv8') || &main::HELP_MESSAGE;
521
522$ST = "\007" if ($opt_b);
523
524$titlestack[ $SP = 0 ] = "unknown";
525$item_stack[$SP] = 0;
526$mode_stack[$SP] = $TM = 0;
527
528binmode( STDOUT, ":utf8" ) if ( $encoding eq "utf-8" );
529if ($opt_8) {
530    if ( $encoding eq "utf-8" ) {
531        undef $opt_8;
532        printf "...ignoring -8 option since locale uses %s\n", $encoding;
533    }
534    else {
535        printf STDERR "\x1b G";
536        $CSI = "\x9b";
537        $OSC = "\x9d";
538        $ST  = "\x9c";
539    }
540}
541
542&get_WM_NAME;
543
544&raw;
545&raw;
546while (1) {
547    my $cmd;
548
549    printf "\r\nCommand (? for help):";
550    $cmd = ReadKey 0;
551    if ( not $cmd ) {
552        sleep 1;
553    }
554    elsif ( $cmd eq "?" ) {
555        printf "\r\n? help,"
556          . " d=display,"
557          . " m/M=set/reset mode,"
558          . " p=set title,"
559          . " q=quit,"
560          . " r=restore,"
561          . " s=save\r\n";
562    }
563    elsif ( $cmd eq "#" ) {
564        printf " ...comment\r\n\t#";
565        &cooked;
566        ReadLine 0;
567        &raw;
568    }
569    elsif ( $cmd eq "!" ) {
570        printf " ...shell\r\n";
571        &cooked;
572        system( $ENV{SHELL} );
573        &raw;
574    }
575    elsif ( $cmd eq "d" ) {
576        printf " ...display\r\n";
577        &display_info;
578    }
579    elsif ( $cmd eq "p" ) {
580        printf " ...set text\r\n";
581        &set_titletext;
582    }
583    elsif ( $cmd eq "q" ) {
584        printf " ...quit\r\n";
585        last;
586    }
587    elsif ( $cmd eq "s" ) {
588        printf " ...save title\r\n";
589        &save_title;
590    }
591    elsif ( $cmd eq "r" ) {
592        printf " ...restore title\r\n";
593        &restore_title(0);
594    }
595    elsif ( $cmd eq "m" ) {
596        printf " ...set title mode\r\n";
597        &set_titlemode(1);
598    }
599    elsif ( $cmd eq "M" ) {
600        printf " ...reset title mode\r\n";
601        &set_titlemode(0);
602    }
603}
604
605# when unstacking here, just use the selector used for the push
606while ( $SP > 0 ) {
607    &restore_title(1);
608}
609
610&send_command( sprintf( "%s>T", $CSI ) );    # reset title-modes to default
611
612&cooked;
613
614printf "\x1b F" if ($opt_8);
615