diff options
author | B. Watson <urchlay@slackware.uk> | 2024-12-26 17:08:34 -0500 |
---|---|---|
committer | B. Watson <urchlay@slackware.uk> | 2024-12-26 17:08:34 -0500 |
commit | 9eed830f296dab257759f5276d0963467007aa6b (patch) | |
tree | 83d6225d248ec8d4026b514d8b9b248e6c0fce32 | |
download | stupid-irssi-tricks-9eed830f296dab257759f5276d0963467007aa6b.tar.gz |
initial commit
-rw-r--r-- | README | 31 | ||||
-rwxr-xr-x | bitmaptext.pl | 2791 | ||||
-rw-r--r-- | colors_per_channel.pl | 103 | ||||
-rw-r--r-- | complete_text.pl | 142 | ||||
-rw-r--r-- | frotzglue.pl | 445 | ||||
-rw-r--r-- | help_path_completion.pl | 43 | ||||
-rw-r--r-- | jumble.pl | 45 | ||||
-rw-r--r-- | newdice.pl | 205 | ||||
-rw-r--r-- | seen.pl | 1201 | ||||
-rw-r--r-- | spaceslash.pl | 35 | ||||
-rw-r--r-- | trap_stdin.pl | 114 | ||||
-rw-r--r-- | unifmt.pl | 611 | ||||
-rw-r--r-- | upsidedown.pl | 242 | ||||
-rw-r--r-- | urlcleaner.pl | 103 | ||||
-rwxr-xr-x | wide.pl | 112 | ||||
-rw-r--r-- | yttitle.pl | 194 |
16 files changed, 6417 insertions, 0 deletions
@@ -0,0 +1,31 @@ +stupid-irssi-tricks: various irssi perl scripts by Urchlay. + +bitmaptext.pl - print giant characters made of UTF-8 boxes +colors_per_channel.pl - Selectively strip colors in text from certain channels/nicks +complete_text.pl - word completion based on channel text +frotzglue.pl - Run a z-code interactive fiction game in a channel +help_path_completion.pl - Support /help word completion for files in help_path +jumble.pl - Smiultae bad tpying +newdice.pl - Die roller that accepts AD&D notation +seen.pl - Tell people when other people were online [*] +spaceslash.pl - treat " /command" as though the space weren't there +trap_stdin.pl - don't lock up irssi if a script tries to read STDIN +unifmt.pl - Fancy Unicode text formatting +upsidedown.pl - print "upside down" text +urlcleaner.pl - trim down ebay and amazon URLs +wide.pl - print double-width characters +yttitle.pl - get titles for youtube videos using yt-dlp + +Some of these scripts are very unpolished, and may not have been +updated for modern irssi. They're in a separate repo partly because +they're not all ready for prime-time, and partly because I refuse to +create a github account. + +[*] The seen.pl script is the only one I didn't write: it's a modified +version of seen.pl from the irssi-scripts github repo, with the Polish +messages translated to English. + +If you have questions, complaints, suggestions, you can find +me on Libera IRC as user Urchlay, or send me email: + +B. Watson <urchlay@slackware.uk> diff --git a/bitmaptext.pl b/bitmaptext.pl new file mode 100755 index 0000000..a7ccf0d --- /dev/null +++ b/bitmaptext.pl @@ -0,0 +1,2791 @@ +#!/usr/bin/perl -w + +use utf8; +use feature 'unicode_strings'; +use open ':std', ':encoding(UTF-8)'; + +$version = "0.1"; + +$DEBUG = 0; + +# Ideas for future features: work as a filter (text on stdin)? support +# for ANSI colors from command line? detect terminal width and do +# word-wrap? use unicode 13.0 semigraphics (2x3 pixel blocks)? these +# aren't well supported by fonts yet... but it would allow our font to +# be higher resolution None of this stuff may ever happen... + +=pod + +=encoding utf8 + +=head1 NAME + +bitmaptext.pl - print giant characters + +=head1 SYNOPSIS + +=head2 From the shell: + +bitmaptext.pl [--help | --man | --version] | [-g] [-i] [ [--test] | [--] [text] ] + +=head2 From within irssi: + +/script load bitmaptext.pl + +/big [-i] [text] + +/giant [-i] [text] + +=head1 DESCRIPTION + +bitmaptext.pl prints text strings rendered as Unicode block +characters, using either the 'quadrant' characters from the 25xx block +to treat each terminal character cell as a block of 2x2 'semigraphics' +pixels, or (with B<-g> option or B</giant> in irssi) the full-block +and space characters for I<really> large characters. + +The font used for rendering is proportional, and uses a B<N> by +8 character cell (where 2 < B<N> < 9; most glyphs are 5x7). Only +printable ASCII (I<U+0020> to I<U+007E>) and Latin-1 (I<U+00A0> to +I<U+00FF>) is supported, plus a few emoji: + +=over 4 + +=item ☮ + +the peace symbol (I<U+262E>) + +=item ☯ + +the yin-yang (I<U+262F>) + +=item ☺ + +the smiley face (I<U+263A>) + +=item ☹ + +the frowny face (I<U+2639>) + +=back + +Other characters will render as spaces. + +When run from the command line, each argument is printed on its own +line. To print a message with a space in it, quote the argument. + +When run from irssi, the entire string is printed as one +line. Standard IRC color codes are supported anywhere in the string +(e.g. B<^C3,4> for green text on red background). Other formatting +(e.g. bold or italic) is not supported. + +Note that the text argument(s) must be Unicode (UTF-8), whether running +standalone or within irssi. + +=head1 OPTIONS + +=head2 Irssi Options + +=over 4 + +=item B<-i> + +Print text in inverse video. Applies to both B</big> and B</giant> +commands. + +=back + +=head2 Command-line Options + +=over 4 + +=item B<-g> + +Print giant text. Same as B</giant> in irssi. + +=item B<-i> + +Print text in inverse video. + +=item B<--> + +Stop processing options; anything after B<--> is taken as the +text to print. Use this if your text begins with I<->. + +=item B<--test> + +Print the entire printable character set (ASCII and Latin-1). Can be +combined with B<-i> and/or B<-g>. Without B<-g>, the table is 43x48 +characters. With B<-g>, it's 90x96. If the table is too big for your +terminal, resize your terminal or try piping it though B<less -S>. + +=item B<--help> + +Print built-in help. + +=item B<--man> + +Print built-in help in man page format. + +=item B<--version> + +Print bitmaptext.pl version number and exit. + +=back + +=head1 EXAMPLES + +These examples may not format properly on your terminal. If they +don't, your terminal probably won't be able to display the large text +from bitmaptext.pl, either. + + $ bitmaptext.pl 'Big Text' + ▄▄ ▗ ▄▄▖ ▖ + ▙▄▘▄ ▞▀▌ ▌ ▞▀▖▌▐▝▛ + ▌ ▌▐ ▚▄▌ ▌ ▛▀▘▞▚ ▌ + ▀▀ ▀▘▗▄▘ ▘ ▝▀ ▘▝ ▝ + + $ bitmaptext.pl -g HEY + + █ █ █████ █ █ + █ █ █ █ █ + █████ ████ █ █ + █ █ █ █ + █ █ █ █ + █ █ █████ █ + + $ bitmaptext.pl -i ' Foobar.' + ███▀▀▜██████▜█████████ + ███▝▀█▚▄▜▚▄▜▝▀█▙▄▜▗▄▜█ + ███▐██▐█▐▐█▐▐█▐▚▄▐▐███ + ███▟██▙▄█▙▄█▄▄█▙▄▟▟██▟ + +=head1 TROUBLESHOOTING + +=head2 Command-line + +If the display in your teminal looks like gibberish, make sure +your terminal supports Unicode and UTF-8 (and has the support +enabled). If you're sure it's enabled but the display is still wrong, +make sure you're using a font that has the Unicode box-drawing +characters. Terminus is a good choice. + +Terminals known to work include B<rxvt-unicode>, B<xterm> (with +e.g. B<-u8> option or UTF-8 enabled in B<~/.Xdefaults>), B<alacritty>, +B<kitty>, B<konsole> (KDE), B<xfce4-terminal> (XFCE). + +The original B<rxvt> is known I<not> to work. The Linux console +may work, but a lot of the available fonts lack some or all of the +quadrant characters (-g should still work). + +If all else fails, try it in B<rxvt-unicode>, which has built-in +box-drawing characters. + +=head2 Irssi + +Make sure irssi is running in a Unicode/UTF-8 capable terminal (see +above). Also run I</set term_charset utf-8> from within irssi. + +If you can see your big text just fine, but another user can't, the +problem is at the other user's end. He has to enable UTF-8 in his +client (and if the client runs in a terminal, he has to use a UTF-8 +capable terminal). + +=head1 NOTES + +There's no way to load the font from an external file; it's built +into the script, although it's at least easy to edit (look at +the bottom of the script file). + +Without B<-g> (or with B</big> in irssi), each character will be +rendered as a B<N> / 2 by 4 block of 2x2 pixel cells, meaning you +can easily exceed the width of your terminal (or your audience's +terminal(s)). Best to stick with messages of 20 characters or less. +Even if you use a really wide terminal window, the people you're +talking to might not. + +With B<-g> (or with B</giant> in irssi), each character will be +rendered as a B<N> by 8 block of single-pixel cells. It's very easy to +exceed the terminal width; best to stick with 10 characters or less. +Also, in irssi, you're sending 8 lines to the server at once: you +may trigger the server's (or the channel's) flood protection and be +temporarily silenced, banned, or even disconnected from the server. +A future version of this script may someday include a delay between +lines to avoid this... + +On IRC, you have no control over how other peoples' clients display +your text. Anyone whose client doesn't support Unicode and UTF-8 won't +be able to see your big and giant messages. Also, with clients that +use a proportional (variable-width) font, the spacing may not come +out right. + +The font was "drawn freehand" in a text editor, not directly based on +any existing font. All glyphs have a blank column on the right, and +most have a blank row at the bottom and top (so the glyphs are mostly +5x6). Lowercase I<g>, I<j>, I<p>, I<q>, and I<y> have true descenders, +meaning they use the bottom row. As with any low-res bitmap fonts, +some compromises have to be made. The lowercase I<a> and I<e> are too +tall, and the centerlines on the I<b>, I<d>, and I<h> are one pixel +lower than they should be. The B<i> and B<j> are too short. Some of +the Latin-1 glyphs are even worse: the diacritics require the letters +to be squashed, and even so, they don't have a blank line on top, so +they'll touch true-descender characters on the line above. + +=head1 AUTHOR + +Urchlay on Libera IRC, <urchlay@slackware.uk> + +=head1 LICENSE + +WTFPL. + +=head1 SEE ALSO + +B<irssi>(1), B<perl>(1), B<urxvt>(1), B<xterm>(1) + +=cut + +# (0,0) (1,0) (0,1) (1,0) order. +%pixel_map = ( + '____' => ' ', + '___X' => '▗', + '__X_' => '▖', + '__XX' => '▄', + '_X__' => '▝', + '_X_X' => '▐', + '_XX_' => '▞', + '_XXX' => '▟', + 'X___' => '▘', + 'X__X' => '▚', + 'X_X_' => '▌', + 'X_XX' => '▙', + 'XX__' => '▀', + 'XX_X' => '▜', + 'XXX_' => '▛', + 'XXXX' => '█', +); + +# add a glyph to the pixels array, on the right. +sub add_glyph_to_pixels { + my $chr = shift; # character + my $pixels = shift; # array ref + + $chr = " " unless $glyphs{$chr}; + my @g = @{$glyphs{$chr}}; + + for(my $i = 0; $i < @g; $i++) { + $pixels->[$i] .= $g[$i]; + } +} + +# does the 2x2 quadrant-block rendering. arg is the array of +# pixels (which are X and _, not unicode blocks/spaces!) +# return value is an array of 3 ready-to-print lines of unicode. +# when called with no args, returns the height of the output, in +# screen lines (font size divided by two). +sub render_big_pixels { + return 4 unless @_; + + my @output; + + my $input_width = length($_[0]); + + # if width is odd, add an extra column of 0 pixels to make it even. + # have to do this because we use 2x2 blocks (can't have 1x2 blocks + # left at the end). + if($input_width % 2) { + s/$/_/ for @_; + } + + for(my $y = 0; $y < @_; $y += 2) { + my $line; + for(my $x = 0; $x < $input_width; $x += 2) { + # (0,0) (1,0) (0,1) (1,0) order. + # e.g. the key for ▚ is X__X. + my $key = substr($_[$y], $x, 2) . substr($_[$y + 1], $x, 2); + $line .= $pixel_map{$key}; + } + push @output, $line; + } + return @output; +} + +# block/space (giant, -g) rendering. very simple: show a solid block +# for a 1 pixel, or a space for 0. +sub render_giant_pixels { + return 8 unless @_; + my @output = @_; + for(@output) { + s/X/█/g; + s/_/ /g; + } + return @output; +} + +# support for -i option: swap the 1's and 0's in the pixels array. +sub invert { + for(@{$_[0]}) { + s/X/#/g; + s/_/X/g; + s/#/_/g; + } +} + +# called when running from command line, not irssi. +sub print_string { + my $renderer = shift; # either \&render_big_pixels or \&render_giant_pixels + my $inverse = shift; # boolean + + my @pixels; + + utf8::decode($_[0]); + for my $c (split "", $_[0]) { + add_glyph_to_pixels($c, \@pixels); + } + + invert(\@pixels) if $inverse; + + my @output = $renderer->(@pixels); + print join("\n", @output), "\n"; +} + +# --test mode, prints whole charset +sub print_font { + my($renderer, $inverse) = @_; + my $termheight = 25; + my $col = 0; + + my(@ascii, @latin); + for(my $i = 32; $i < 128; $i += 16) { + my $a; + $a .= chr($_) for($i .. ($i + 15)); + push @ascii, $a; + $a = ""; + $a .= chr($_) for(($i + 128) .. (($i + 128) + 15)); + push @latin, $a; + } + + print_string($renderer, $inverse, $_) for @ascii, @latin; +} + +# main() if we're not running under irssi +if(__PACKAGE__ eq 'main') { + exec "perldoc $0" if @ARGV && $ARGV[0] =~ /--?h(elp)?/; + exec "pod2man --stderr --utf8 -s1 -r$version -c\"Urchlay's Misc Stuff\" $0" if @ARGV && $ARGV[0] =~ /--?m(an)?/; + + if(@ARGV && $ARGV[0] eq '--version') { + print "bitmaptext.pl $version\n"; + exit 0; + } + + init_font(); + + my $renderer = \&render_big_pixels; + my $inverse = 0; + my $testmode = 0; + + while(@ARGV) { + if($ARGV[0] eq '-g') { + $renderer = \&render_giant_pixels; + shift; + } elsif($ARGV[0] eq '-i') { + $inverse++; + shift; + } elsif($ARGV[0] eq '--test') { + shift; + $testmode++; + } elsif($ARGV[0] eq '--') { + shift; + last; + } elsif($ARGV[0] =~ /^-/) { + die "bitmaptext.pl: unknown option '$ARGV[0]'. Try running with --help.\n"; + } else { + last; + } + } + + if($testmode) { + print_font($renderer, $inverse); + } else { + die "bitmaptext.pl: no text. Try running with --help.\n" unless @ARGV; + print_string($renderer, $inverse, $_) for @ARGV; + } + + exit 0; +} + +# irssi stuff here +init_font(); + +require Irssi; +Irssi->import(qw/command command_bind/); + +our $VERSION = $version; +our %IRSSI = ( + authors => 'Urchlay', + contact => 'Urchlay on Libera', + name => 'bitmaptext', + description => 'print giant characters made of UTF-8 boxes', + license => 'WTFPL', + url => 'none', + ); + +sub irssi_print_string { + my ($text, $srv, $chan, $renderer) = @_; + + return unless $text; + return unless $srv; + return unless $chan; + + my $inverse = 0; + if($text =~ s/^-i\s//) { + $inverse++; + } + + my $height = $renderer->(); + my @lines; + for(my $i = 0; $i < $height; $i++) { + $lines[$i] = ""; + } + + # irssi's written in C, it passes us strings of utf-8 bytes, but in perl + # there's a difference between e.g. "\xc2\xa2" (2 bytes, utf8-encoded) and + # the string representation of the utf-8 character "¢" (written "\x{a2}"). + # utf8::decode turns the former into the latter, so when we split the + # string to get the characters, we get unicode characters. + utf8::decode($text); + + # Color spec: + # https://modern.ircdocs.horse/formatting.html + # we don't have to interpret the color codes, just recognize and + # append to output lines. + + for my $i (split /(\x03[0-9]+(?:,[0-9]+)?|\x03)/, $text) { + if($i =~ /^\x03/) { + if($DEBUG) { + if($i eq "\x03") { + print "color off"; + } else { + print "color " . substr($i, 1); + } + } + map { $_ .= $i } @lines; + } else { + print "word: $i" if $DEBUG; + my @pixels; + for my $ch (split "", $i) { + print "glyph: $ch" if $DEBUG; + add_glyph_to_pixels($ch, \@pixels); + } + invert(\@pixels) if $inverse; + my @r = $renderer->(@pixels); + for(my $l = 0; $l < @r; $l++) { + $lines[$l] .= $r[$l]; + } + } + } + + for(@lines) { + $chan->command('MSG ' . $chan->{name} . ' ' . $_); + } +} + +sub cmd_big { + irssi_print_string(@_, \&render_big_pixels); +} + +sub cmd_giant { + irssi_print_string(@_, \&render_giant_pixels); +} + +# the help for our 2 options is very similar, use this as a template to +# create them both. +$helptext = <<EOF; +Syntax: + +/_CMD_ [-i] text... + +Parameters: + + -i: Inverse video. + +Description: + +Prints big text to current channel/query, _DESC_ + +Examples: + + /_CMD_ Hello + /_CMD_ -i Hi there + +References: + + <TODO> + +See also: _OTHERCMD_ +EOF + +sub mkhelp { + my $text = $helptext; + my $cmd = $_[0]; + + for(qw/_CMD_ _DESC_ _OTHERCMD_/) { + $text =~ s/$_/$_[0]/gm; + shift; + } + + # \x02 is ^B, IRC bold toggle. use it for e.g. Syntax:, Parameters:... + $text =~ s/^.*:/\x02$&\x02/gm; + + $help{$cmd} = $text; +} + +mkhelp("big", "using Unicode quadrant blocks as pixels.", "giant"); +mkhelp("giant", "using Unicode full blocks and spaces as pixels.", "big"); + +# write the generated help to files, so irssi's /help command will +# find them. this relies on the user's help_path setting, can't remember +# what the default is... +my $dir = Irssi::get_irssi_dir() . "/help"; +if(! -e $dir) { + mkdir $dir; +} +if(-e $dir) { + for(keys %help) { + if(open my $f, ">", "$dir/$_") { + print $f $help{$_}; + close $f; + } + } +} + +command_bind("big", \&cmd_big); +command_bind("giant", \&cmd_giant); + +# call init_font once at startup. it's a function instead of mainline code so +# we can avoid calling it when we're run with --help. +# +# if you're editing the font, the rules are: +# +# 1. glyphs are variable width Nx8 monochrome bitmaps. there's no limit on N, +# but most of the glyphs are 6x8 (5px wide plus a blank column), so you +# don't want to make your new glyphs too much wider than that. +# 2. all rows in the same glyph must be the same width. +# 3. use an X for a set pixel, use _ for an unset pixel. +# 4. the rightmost column must be all _, so the characters don't +# touch each other. +# +# There's a bit of error-checking that make the script die if you +# break the rules, but it may not be infallible. +# Also, not a hard-and-fast-rule, but the bottom row will normally +# be blank, except for glyphs with true descenders (lowercase g j q y). +# You can assign a glyph to any Unicode character. There's no requirement +# for it to be ASCII or Latin-1. + +sub init_font { + # ASCII printable range (32 to 126). + @{$glyphs{" "}} = qw/ + ______ + ______ + ______ + ______ + ______ + ______ + ______ + ______ + /; + + @{$glyphs{"!"}} = qw/ + __ + X_ + X_ + X_ + X_ + __ + X_ + __ + /; + + @{$glyphs{"\""}} = qw/ + ____ + X_X_ + X_X_ + ____ + ____ + ____ + ____ + ____ + /; + + @{$glyphs{"#"}} = qw/ + ______ + ______ + _X_X__ + XXXXX_ + _X_X__ + XXXXX_ + _X_X__ + ______ + /; + + @{$glyphs{"\$"}} = qw/ + ______ + __X___ + _XXXX_ + X_X___ + _XXX__ + __X_X_ + XXXX__ + __X___ + /; + + @{$glyphs{"%"}} = qw/ + ______ + ______ + XX__X_ + ___X__ + __X___ + _X____ + X__XX_ + ______ + /; + + @{$glyphs{"&"}} = qw/ + _____ + __X__ + _XXX_ + X____ + _XX__ + X____ + _XXX_ + __X__ + /; + +## @{$glyphs{"&"}} = qw/ +## _______ +## _XXX___ +## X___X__ +## _XXX___ +## X__XXX_ +## X___X__ +## _XXX_X_ +## _______ +## /; +## + @{$glyphs{"'"}} = qw/ + __ + X_ + X_ + __ + __ + __ + __ + __ + /; + + @{$glyphs{"("}} = qw/ + ___ + _X_ + X__ + X__ + X__ + X__ + _X_ + ___ + /; + + @{$glyphs{")"}} = qw/ + ___ + X__ + _X_ + _X_ + _X_ + _X_ + X__ + ___ + /; + + @{$glyphs{"*"}} = qw/ + ______ + ______ + ______ + X_X_X_ + _XXX__ + X_X_X_ + ______ + ______ + /; + + @{$glyphs{"+"}} = qw/ + ____ + ____ + ____ + _X__ + XXX_ + _X__ + ____ + ____ + /; + + @{$glyphs{","}} = qw/ + ___ + ___ + ___ + ___ + ___ + _X_ + X__ + ___ + /; + + @{$glyphs{"-"}} = qw/ + ____ + ____ + ____ + ____ + XXX_ + ____ + ____ + ____ + /; + + @{$glyphs{"."}} = qw/ + __ + __ + __ + __ + __ + __ + X_ + __ + /; + + @{$glyphs{"/"}} = qw/ + ______ + ______ + ____X_ + ___X__ + __X___ + _X____ + X_____ + ______ + /; + + @{$glyphs{"0"}} = qw/ + ______ + _XXX__ + X__XX_ + X_X_X_ + XX__X_ + X___X_ + _XXX__ + ______ + /; + + @{$glyphs{"1"}} = qw/ + ____ + _X__ + XX__ + _X__ + _X__ + _X__ + XXX_ + ____ + /; + + @{$glyphs{"2"}} = qw/ + ______ + XXXX__ + ____X_ + _XXX__ + X_____ + X_____ + XXXXX_ + ______ + /; + + @{$glyphs{"3"}} = qw/ + ______ + XXXX__ + ____X_ + _XXX__ + ____X_ + ____X_ + XXXX__ + ______ + /; + + @{$glyphs{"4"}} = qw/ + ______ + __X_X_ + _X__X_ + X___X_ + XXXXX_ + ____X_ + ____X_ + ______ + /; + + @{$glyphs{"5"}} = qw/ + ______ + XXXXX_ + X_____ + XXXX__ + ____X_ + ____X_ + XXXX__ + ______ + /; + + @{$glyphs{"6"}} = qw/ + ______ + _XXX__ + X_____ + XXXX__ + X___X_ + X___X_ + _XXX__ + ______ + /; + + @{$glyphs{"7"}} = qw/ + ______ + XXXXX_ + ____X_ + ___X__ + __X___ + __X___ + __X___ + ______ + /; + + @{$glyphs{"8"}} = qw/ + ______ + _XXX__ + X___X_ + _XXX__ + X___X_ + X___X_ + _XXX__ + ______ + /; + + @{$glyphs{"9"}} = qw/ + ______ + _XXX__ + X___X_ + X___X_ + _XXXX_ + ____X_ + _XXX__ + ______ + /; + + @{$glyphs{":"}} = qw/ + __ + __ + __ + X_ + __ + X_ + __ + __ + /; + + @{$glyphs{";"}} = qw/ + ___ + ___ + ___ + _X_ + ___ + _X_ + X__ + ___ + /; + + @{$glyphs{"<"}} = qw/ + ______ + ______ + ____X_ + __XX__ + XX____ + __XX__ + ____X_ + ______ + /; + + @{$glyphs{"="}} = qw/ + ____ + ____ + ____ + XXX_ + ____ + XXX_ + ____ + ____ + /; + + @{$glyphs{">"}} = qw/ + ______ + ______ + X_____ + _XX___ + ___XX_ + _XX___ + X_____ + ______ + /; + + @{$glyphs{"?"}} = qw/ + _XXX__ + X___X_ + ____X_ + ___X__ + __X___ + ______ + __X___ + ______ + /; + + @{$glyphs{"@"}} = qw/ + ______ + ______ + _XXX__ + X_X_X_ + X_XXX_ + X_____ + _XXXX_ + ______ + /; + + @{$glyphs{"A"}} = qw/ + ______ + __X___ + _X_X__ + X___X_ + XXXXX_ + X___X_ + X___X_ + ______ + /; + + @{$glyphs{"B"}} = qw/ + ______ + XXXX__ + X___X_ + XXXX__ + X___X_ + X___X_ + XXXX__ + ______ + /; + + @{$glyphs{"C"}} = qw/ + ______ + _XXX__ + X___X_ + X_____ + X_____ + X___X_ + _XXX__ + ______ + /; + + @{$glyphs{"D"}} = qw/ + ______ + XXXX__ + X___X_ + X___X_ + X___X_ + X___X_ + XXXX__ + ______ + /; + + @{$glyphs{"E"}} = qw/ + ______ + XXXXX_ + X_____ + XXXX__ + X_____ + X_____ + XXXXX_ + ______ + /; + + @{$glyphs{"F"}} = qw/ + ______ + XXXXX_ + X_____ + XXXX__ + X_____ + X_____ + X_____ + ______ + /; + + @{$glyphs{"G"}} = qw/ + ______ + _XXXX_ + X_____ + X_____ + X__XX_ + X___X_ + _XXX__ + ______ + /; + + @{$glyphs{"H"}} = qw/ + ______ + X___X_ + X___X_ + XXXXX_ + X___X_ + X___X_ + X___X_ + ______ + /; + + @{$glyphs{"I"}} = qw/ + ____ + XXX_ + _X__ + _X__ + _X__ + _X__ + XXX_ + ____ + /; + + @{$glyphs{"J"}} = qw/ + ______ + ___XX_ + ____X_ + ____X_ + ____X_ + X___X_ + _XXX__ + ______ + /; + + @{$glyphs{"K"}} = qw/ + ______ + X___X_ + X__X__ + XXX___ + X__X__ + X___X_ + X___X_ + ______ + /; + + @{$glyphs{"L"}} = qw/ + ______ + X_____ + X_____ + X_____ + X_____ + X_____ + XXXXX_ + ______ + /; + + @{$glyphs{"M"}} = qw/ + ______ + X___X_ + XX_XX_ + X_X_X_ + X_X_X_ + X___X_ + X___X_ + ______ + /; + + @{$glyphs{"N"}} = qw/ + ______ + X___X_ + XX__X_ + X_X_X_ + X_X_X_ + X__XX_ + X___X_ + ______ + /; + + @{$glyphs{"O"}} = qw/ + ______ + _XXX__ + X___X_ + X___X_ + X___X_ + X___X_ + _XXX__ + ______ + /; + + @{$glyphs{"P"}} = qw/ + ______ + XXXX__ + X___X_ + X___X_ + XXXX__ + X_____ + X_____ + ______ + /; + + @{$glyphs{"Q"}} = qw/ + ______ + _XXX__ + X___X_ + X___X_ + X___X_ + X__X__ + _XX_X_ + ______ + /; + + @{$glyphs{"R"}} = qw/ + ______ + XXXX__ + X___X_ + X___X_ + XXXX__ + X___X_ + X___X_ + ______ + /; + + @{$glyphs{"S"}} = qw/ + ______ + _XXXX_ + X_____ + _XXX__ + ____X_ + ____X_ + XXXX__ + ______ + /; + + @{$glyphs{"T"}} = qw/ + ______ + XXXXX_ + __X___ + __X___ + __X___ + __X___ + __X___ + ______ + /; + + @{$glyphs{"U"}} = qw/ + ______ + X___X_ + X___X_ + X___X_ + X___X_ + X___X_ + _XXX__ + ______ + /; + + @{$glyphs{"V"}} = qw/ + ______ + X___X_ + X___X_ + X___X_ + X___X_ + _X_X__ + __X___ + ______ + /; + + @{$glyphs{"W"}} = qw/ + ______ + X___X_ + X___X_ + X_X_X_ + X_X_X_ + XX_XX_ + _X_X__ + ______ + /; + + @{$glyphs{"X"}} = qw/ + ______ + X___X_ + _X_X__ + __X___ + _X_X__ + X___X_ + X___X_ + ______ + /; + + @{$glyphs{"Y"}} = qw/ + ______ + X___X_ + X___X_ + _X_X__ + __X___ + __X___ + __X___ + ______ + /; + + @{$glyphs{"Z"}} = qw/ + ______ + XXXXX_ + ___X__ + __X___ + _X____ + X_____ + XXXXX_ + ______ + /; + + @{$glyphs{"["}} = qw/ + ____ + XXX_ + X___ + X___ + X___ + X___ + XXX_ + ____ + /; + + @{$glyphs{"\\"}} = qw/ + ______ + ______ + X_____ + _X____ + __X___ + ___X__ + ____X_ + ______ + /; + + @{$glyphs{"]"}} = qw/ + ____ + XXX_ + __X_ + __X_ + __X_ + __X_ + XXX_ + ____ + /; + + @{$glyphs{"^"}} = qw/ + ____ + _X__ + X_X_ + ____ + ____ + ____ + ____ + ____ + /; + + @{$glyphs{"_"}} = qw/ + ______ + ______ + ______ + ______ + ______ + ______ + XXXXX_ + ______ + /; + + @{$glyphs{"`"}} = qw/ + ___ + X__ + _X_ + ___ + ___ + ___ + ___ + ___ + /; + + @{$glyphs{"a"}} = qw/ + ______ + ______ + _XXX__ + ____X_ + _XXXX_ + X___X_ + _XXXX_ + ______ + /; + + @{$glyphs{"b"}} = qw/ + ______ + X_____ + X_____ + XXXX__ + X___X_ + X___X_ + XXXX__ + ______ + /; + + @{$glyphs{"c"}} = qw/ + _____ + _____ + _XXX_ + X____ + X____ + X____ + _XXX_ + _____ + /; + + @{$glyphs{"d"}} = qw/ + ______ + ____X_ + ____X_ + _XXXX_ + X___X_ + X___X_ + _XXXX_ + ______ + /; + + @{$glyphs{"e"}} = qw/ + ______ + ______ + _XXX__ + X___X_ + XXXXX_ + X_____ + _XXX__ + ______ + /; + + @{$glyphs{"f"}} = qw/ + _____ + __XX_ + _X___ + XXX__ + _X___ + _X___ + _X___ + _____ + /; + + @{$glyphs{"g"}} = qw/ + ______ + ______ + _XXXX_ + X___X_ + X___X_ + _XXXX_ + ____X_ + _XXX__ + /; + + @{$glyphs{"h"}} = qw/ + ______ + X_____ + X_____ + XXXX__ + X___X_ + X___X_ + X___X_ + ______ + /; + + @{$glyphs{"i"}} = qw/ + ____ + _X__ + ____ + XX__ + _X__ + _X__ + XXX_ + ____ + /; + + @{$glyphs{"j"}} = qw/ + ____ + __X_ + ____ + _XX_ + __X_ + __X_ + __X_ + XX__ + /; + + @{$glyphs{"k"}} = qw/ + _____ + X____ + X__X_ + X_X__ + XX___ + X_X__ + X__X_ + _____ + /; + + @{$glyphs{"l"}} = qw/ + ____ + XX__ + _X__ + _X__ + _X__ + _X__ + _XX_ + ____ + /; + + @{$glyphs{"m"}} = qw/ + ______ + ______ + XX_X__ + X_X_X_ + X_X_X_ + X_X_X_ + X___X_ + ______ + /; + + @{$glyphs{"n"}} = qw/ + ______ + ______ + XXXX__ + X___X_ + X___X_ + X___X_ + X___X_ + ______ + /; + + @{$glyphs{"o"}} = qw/ + ______ + ______ + _XXX__ + X___X_ + X___X_ + X___X_ + _XXX__ + ______ + /; + + @{$glyphs{"p"}} = qw/ + ______ + ______ + XXXX__ + X___X_ + X___X_ + XXXX__ + X_____ + X_____ + /; + + @{$glyphs{"q"}} = qw/ + _______ + _______ + _XXXX__ + X___X__ + X___X__ + _XXXX__ + ____X__ + _____X_ + /; + + @{$glyphs{"r"}} = qw/ + ______ + ______ + XXXX__ + X___X_ + X_____ + X_____ + X_____ + ______ + /; + + @{$glyphs{"s"}} = qw/ + ______ + ______ + _XXXX_ + X_____ + _XXX__ + ____X_ + XXXX__ + ______ + /; + + @{$glyphs{"t"}} = qw/ + ____ + _X__ + XXX_ + _X__ + _X__ + _X__ + __X_ + ____ + /; + + @{$glyphs{"u"}} = qw/ + ______ + ______ + X___X_ + X___X_ + X___X_ + X___X_ + _XXX__ + ______ + /; + + @{$glyphs{"v"}} = qw/ + ______ + ______ + X___X_ + X___X_ + X___X_ + _X_X__ + __X___ + ______ + /; + + @{$glyphs{"w"}} = qw/ + ______ + ______ + X___X_ + X_X_X_ + X_X_X_ + X_X_X_ + _X_X__ + ______ + /; + + @{$glyphs{"x"}} = qw/ + _____ + _____ + X__X_ + X__X_ + _XX__ + X__X_ + X__X_ + _____ + /; + + @{$glyphs{"y"}} = qw/ + ______ + ______ + X___X_ + X___X_ + X___X_ + _X_X__ + __X___ + _X____ + /; + + @{$glyphs{"z"}} = qw/ + ______ + ______ + XXXXX_ + ___X__ + __X___ + _X____ + XXXXX_ + ______ + /; + + @{$glyphs{"{"}} = qw/ + ____ + __X_ + _X__ + _X__ + X___ + _X__ + _X__ + __X_ + /; + + @{$glyphs{"|"}} = qw/ + __ + X_ + X_ + X_ + X_ + X_ + X_ + __ + /; + + @{$glyphs{"}"}} = qw/ + ____ + X___ + _X__ + _X__ + __X_ + _X__ + _X__ + X___ + /; + + @{$glyphs{"~"}} = qw/ + ______ + ______ + ______ + _XX_X_ + X__X__ + ______ + ______ + ______ + /; + + # end of ASCII, start of Latin-1. first char is a non-breaking space. + + @{$glyphs{" "}} = qw/ + ______ + ______ + ______ + ______ + ______ + ______ + ______ + ______ + /; + + @{$glyphs{"¡"}} = qw/ + __ + X_ + __ + X_ + X_ + X_ + X_ + __ + /; + + @{$glyphs{"¢"}} = qw/ + _____ + __X__ + _XXX_ + X_X__ + X_X__ + _XXX_ + __X__ + _____ + /; + + @{$glyphs{"£"}} = qw/ + ______ + __XX__ + _X__X_ + X_____ + _XX___ + X_____ + XXXXX_ + ______ + /; + + @{$glyphs{"¤"}} = qw/ + ______ + ______ + X___X_ + _XXX__ + _X_X__ + _XXX__ + X___X_ + ______ + /; + + @{$glyphs{"¥"}} = qw/ + ______ + X___X_ + _X_X__ + XXXXX_ + __X___ + XXXXX_ + __X___ + ______ + /; + + @{$glyphs{"¦"}} = qw/ + __ + X_ + X_ + X_ + __ + X_ + X_ + X_ + /; + + @{$glyphs{"§"}} = qw/ + ______ + __XX__ + _X____ + __X___ + _X_X__ + __X___ + ___X__ + _XX___ + /; + + @{$glyphs{"¨"}} = qw/ + _____ + X__X_ + _____ + _____ + _____ + _____ + _____ + _____ + /; + + @{$glyphs{"©"}} = qw/ + ________ + _XXXXX__ + X_____X_ + X__XX_X_ + X_X___X_ + X__XX_X_ + X_____X_ + _XXXXX__ + /; + + @{$glyphs{"ª"}} = qw/ + _______ + _XXX___ + ___XX__ + _XX_X__ + __XXX__ + _______ + XXXXXX_ + _______ + /; + + @{$glyphs{"«"}} = qw/ + ______ + ______ + ______ + ______ + _X__X_ + X__X__ + _X__X_ + ______ + /; + + @{$glyphs{"¬"}} = qw/ + ______ + ______ + ______ + ______ + ______ + XXXXX_ + ____X_ + ______ + /; + + @{$glyphs{""}} = qw/ + ____ + ____ + ____ + ____ + XXX_ + ____ + ____ + ____ + /; + + @{$glyphs{"®"}} = qw/ + _XXXXX__ + X_____X_ + X_XX__X_ + X_X_X_X_ + X_XX__X_ + X_X_X_X_ + X_____X_ + _XXXXX__ + /; + + @{$glyphs{"¯"}} = qw/ + _____ + XXXX_ + _____ + _____ + _____ + _____ + _____ + _____ + /; + + @{$glyphs{"°"}} = qw/ + ______ + _XXX__ + X___X_ + _XXX__ + ______ + ______ + ______ + ______ + /; + + @{$glyphs{"±"}} = qw/ + ______ + ______ + __X___ + XXXXX_ + __X___ + ______ + XXXXX_ + ______ + /; + + @{$glyphs{"²"}} = qw/ + ____ + XX__ + __X_ + _X__ + XXX_ + ____ + ____ + ____ + /; + + @{$glyphs{"³"}} = qw/ + ____ + XXX_ + _XX_ + XXX_ + ____ + ____ + ____ + ____ + /; + + @{$glyphs{"´"}} = qw/ + ___ + _X_ + X__ + ___ + ___ + ___ + ___ + ___ + /; + + @{$glyphs{"µ"}} = qw/ + ______ + ______ + X___X_ + X___X_ + X___X_ + XX__X_ + X_XX__ + X_____ + /; + + @{$glyphs{"¶"}} = qw/ + ______ + _XX_X_ + XXX_X_ + _XX_X_ + __X_X_ + __X_X_ + __X_X_ + ______ + /; + + @{$glyphs{"·"}} = qw/ + __ + __ + __ + __ + X_ + __ + __ + __ + /; + + @{$glyphs{"¸"}} = qw/ + ____ + ____ + ____ + ____ + ____ + _X__ + __X_ + _X__ + /; + + @{$glyphs{"¹"}} = qw/ + ____ + XX__ + _X__ + XXX_ + ____ + ____ + ____ + ____ + /; + + @{$glyphs{"º"}} = qw/ + ______ + ______ + ______ + _XXX__ + X___X_ + _XXX__ + ______ + ______ + /; + + @{$glyphs{"»"}} = qw/ + ______ + ______ + ______ + ______ + X__X__ + _X__X_ + X__X__ + ______ + /; + + @{$glyphs{"¼"}} = qw/ + _______ + XX_____ + _X_____ + XXX____ + _______ + ___X_X_ + ___XXX_ + _____X_ + /; + + @{$glyphs{"½"}} = qw/ + _______ + XX_____ + _X_____ + XXX____ + ____XX_ + _____X_ + ____X__ + ____XX_ + /; + + @{$glyphs{"¾"}} = qw/ + ________ + XXX_____ + _XX_____ + XXX_____ + ________ + ____X_X_ + ____XXX_ + ______X_ + /; + + @{$glyphs{"¿"}} = qw/ + __X___ + ______ + __X___ + _X____ + X_____ + X___X_ + _XXX__ + ______ + /; + + @{$glyphs{"À"}} = qw/ + _X____ + __X___ + ______ + _XXX__ + X___X_ + XXXXX_ + X___X_ + ______ + /; + + @{$glyphs{"Á"}} = qw/ + ___X__ + __X___ + ______ + _XXX__ + X___X_ + XXXXX_ + X___X_ + ______ + /; + + @{$glyphs{"Â"}} = qw/ + __X___ + _X_X__ + ______ + _XXX__ + X___X_ + XXXXX_ + X___X_ + ______ + /; + + @{$glyphs{"Ã"}} = qw/ + __XX_X_ + _X__X__ + _______ + _XXX___ + X___X__ + XXXXX__ + X___X__ + _______ + /; + + @{$glyphs{"Ä"}} = qw/ + _X_X__ + ______ + __X___ + _X_X__ + X___X_ + XXXXX_ + X___X_ + ______ + /; + + @{$glyphs{"Å"}} = qw/ + __X____ + _X_X___ + __X____ + _X_X___ + X___X__ + XXXXX__ + X___X__ + _______ + /; + + @{$glyphs{"Æ"}} = qw/ + ______ + __XXX_ + _X_X__ + X__X__ + XXXXX_ + X__X__ + X__XX_ + ______ + /; + + @{$glyphs{"Ç"}} = qw/ + ______ + _XXXX_ + X_____ + X_____ + X_____ + _XXXX_ + ___X__ + __X___ + /; + + @{$glyphs{"È"}} = qw/ + _X____ + __X___ + XXXXX_ + X_____ + XXXX__ + X_____ + XXXXX_ + ______ + /; + + @{$glyphs{"É"}} = qw/ + ___X__ + __X___ + XXXXX_ + X_____ + XXXX__ + X_____ + XXXXX_ + ______ + /; + + @{$glyphs{"Ê"}} = qw/ + __X___ + _X_X__ + XXXXX_ + X_____ + XXXX__ + X_____ + XXXXX_ + ______ + /; + + @{$glyphs{"Ë"}} = qw/ + _X_X__ + ______ + XXXXX_ + X_____ + XXXX__ + X_____ + XXXXX_ + ______ + /; + + @{$glyphs{"Ì"}} = qw/ + X___ + _X__ + ____ + XXX_ + _X__ + _X__ + XXX_ + ____ + /; + + @{$glyphs{"Í"}} = qw/ + __X_ + _X__ + ____ + XXX_ + _X__ + _X__ + XXX_ + ____ + /; + + @{$glyphs{"Î"}} = qw/ + _X__ + X_X_ + ____ + XXX_ + _X__ + _X__ + XXX_ + ____ + /; + + @{$glyphs{"Ï"}} = qw/ + ____ + X_X_ + ____ + XXX_ + _X__ + _X__ + XXX_ + ____ + /; + + @{$glyphs{"Ð"}} = qw/ + ______ + XXXX__ + X___X_ + X___X_ + XXX_X_ + X___X_ + XXXX__ + ______ + /; + + @{$glyphs{"Ñ"}} = qw/ + __X_X_ + _X_X__ + X___X_ + XX__X_ + X_X_X_ + X__XX_ + X___X_ + ______ + /; + + @{$glyphs{"Ò"}} = qw/ + _X____ + __X___ + _XXX__ + X___X_ + X___X_ + X___X_ + _XXX__ + ______ + /; + + @{$glyphs{"Ó"}} = qw/ + ___X__ + __X___ + _XXX__ + X___X_ + X___X_ + X___X_ + _XXX__ + ______ + /; + + @{$glyphs{"Ô"}} = qw/ + __X___ + _X_X__ + _XXX__ + X___X_ + X___X_ + X___X_ + _XXX__ + ______ + /; + + @{$glyphs{"Õ"}} = qw/ + __XX_X_ + _X__X__ + _XXX___ + X___X__ + X___X__ + X___X__ + _XXX___ + _______ + /; + + @{$glyphs{"Ö"}} = qw/ + _X_X__ + ______ + _XXX__ + X___X_ + X___X_ + X___X_ + _XXX__ + ______ + /; + + @{$glyphs{"×"}} = qw/ + _____ + _____ + _____ + X__X_ + _XX__ + X__X_ + _____ + _____ + /; + + @{$glyphs{"Ø"}} = qw/ + ________ + __XXX_X_ + _X___X__ + _X__XX__ + _X_X_X__ + _XX__X__ + X_XXX___ + ________ + /; + + @{$glyphs{"Ù"}} = qw/ + _X____ + __X___ + X___X_ + X___X_ + X___X_ + X___X_ + _XXX__ + ______ + /; + + @{$glyphs{"Ú"}} = qw/ + ___X__ + __X___ + X___X_ + X___X_ + X___X_ + X___X_ + _XXX__ + ______ + /; + + @{$glyphs{"Û"}} = qw/ + ___X____ + __X_X___ + X_____X_ + X_____X_ + X_____X_ + X_____X_ + _XXXXX__ + ________ + /; + + @{$glyphs{"Ü"}} = qw/ + _X_X__ + ______ + X___X_ + X___X_ + X___X_ + X___X_ + _XXX__ + ______ + /; + + @{$glyphs{"Ý"}} = qw/ + ___X__ + __X___ + X___X_ + _X_X__ + __X___ + __X___ + __X___ + ______ + /; + + @{$glyphs{"Þ"}} = qw/ + ______ + ______ + X_____ + XXXX__ + X___X_ + XXXX__ + X_____ + ______ + /; + + @{$glyphs{"ß"}} = qw/ + ______ + _XXX__ + X___X_ + XXXX__ + X___X_ + X___X_ + X_XX__ + ______ + /; + + @{$glyphs{"à"}} = qw/ + _X____ + __X___ + _XXX__ + ____X_ + _XXXX_ + X___X_ + _XXXX_ + ______ + /; + + @{$glyphs{"á"}} = qw/ + ___X__ + __X___ + _XXX__ + ____X_ + _XXXX_ + X___X_ + _XXXX_ + ______ + /; + + @{$glyphs{"â"}} = qw/ + __X___ + _X_X__ + _XXX__ + ____X_ + _XXXX_ + X___X_ + _XXXX_ + ______ + /; + + @{$glyphs{"ã"}} = qw/ + __XX_X_ + _X__X__ + _______ + _XXX___ + ___XX__ + _XX_X__ + __XXX__ + _______ + /; + + @{$glyphs{"ä"}} = qw/ + _X_X__ + ______ + _XXX__ + ____X_ + _XXXX_ + X___X_ + _XXXX_ + ______ + /; + + @{$glyphs{"å"}} = qw/ + __XX___ + _X__X__ + __XX___ + _XXX___ + ___XX__ + _XX_X__ + __XXX__ + _______ + /; + + @{$glyphs{"æ"}} = qw/ + ________ + ________ + _XX_XX__ + ___X__X_ + _XXXXXX_ + X__X____ + _XX_XXX_ + ________ + /; + + @{$glyphs{"ç"}} = qw/ + _____ + _____ + _XXX_ + X____ + X____ + _XXX_ + __X__ + _X___ + /; + + @{$glyphs{"è"}} = qw/ + _X____ + __X___ + _XXX__ + X___X_ + XXXXX_ + X_____ + _XXX__ + ______ + /; + + @{$glyphs{"é"}} = qw/ + ___X__ + __X___ + _XXX__ + X___X_ + XXXXX_ + X_____ + _XXX__ + ______ + /; + + @{$glyphs{"ê"}} = qw/ + __X___ + _X_X__ + _XXX__ + X___X_ + XXXXX_ + X_____ + _XXX__ + ______ + /; + + @{$glyphs{"ë"}} = qw/ + _X_X__ + ______ + _XXX__ + X___X_ + XXXXX_ + X_____ + _XXX__ + ______ + /; + + @{$glyphs{"ì"}} = qw/ + X___ + _X__ + ____ + XX__ + _X__ + _X__ + XXX_ + ____ + /; + + @{$glyphs{"í"}} = qw/ + __X_ + _X__ + ____ + XX__ + _X__ + _X__ + XXX_ + ____ + /; + + @{$glyphs{"î"}} = qw/ + _X__ + X_X_ + ____ + XX__ + _X__ + _X__ + XXX_ + ____ + /; + + @{$glyphs{"ï"}} = qw/ + ____ + X_X_ + ____ + XX__ + _X__ + _X__ + XXX_ + ____ + /; + + @{$glyphs{"ð"}} = qw/ + X_X___ + _XX___ + X__X__ + ____X_ + _XXXX_ + X___X_ + _XXXX_ + ______ + /; + + @{$glyphs{"ñ"}} = qw/ + _XX_X_ + X__X__ + ______ + XXXX__ + X___X_ + X___X_ + X___X_ + ______ + /; + + @{$glyphs{"ò"}} = qw/ + _X____ + __X___ + ______ + _XXX__ + X___X_ + X___X_ + _XXX__ + ______ + /; + + @{$glyphs{"ó"}} = qw/ + ___X__ + __X___ + ______ + _XXX__ + X___X_ + X___X_ + _XXX__ + ______ + /; + + @{$glyphs{"ô"}} = qw/ + __X___ + _X_X__ + ______ + _XXX__ + X___X_ + X___X_ + _XXX__ + ______ + /; + + @{$glyphs{"õ"}} = qw/ + __XX_X_ + _X__X__ + _______ + _XXX___ + X___X__ + X___X__ + _XXX___ + _______ + /; + + @{$glyphs{"ö"}} = qw/ + ______ + _X_X__ + ______ + _XXX__ + X___X_ + X___X_ + _XXX__ + ______ + /; + + @{$glyphs{"÷"}} = qw/ + ____ + ____ + _X__ + ____ + XXX_ + ____ + _X__ + ____ + /; + + @{$glyphs{"ø"}} = qw/ + ________ + ________ + __XXX_X_ + _X__XX__ + _X_X_X__ + _XX__X__ + X_XXX___ + ________ + /; + + @{$glyphs{"ù"}} = qw/ + _X____ + __X___ + ______ + X___X_ + X___X_ + X___X_ + _XXX__ + ______ + /; + + @{$glyphs{"ú"}} = qw/ + ___X__ + __X___ + ______ + X___X_ + X___X_ + X___X_ + _XXX__ + ______ + /; + + @{$glyphs{"û"}} = qw/ + __X___ + _X_X__ + ______ + X___X_ + X___X_ + X___X_ + _XXX__ + ______ + /; + + @{$glyphs{"ü"}} = qw/ + ______ + _X__X_ + ______ + X___X_ + X___X_ + X___X_ + _XXX__ + ______ + /; + + @{$glyphs{"ý"}} = qw/ + ______ + ___X__ + __X___ + X___X_ + X___X_ + _X_X__ + __X___ + _X____ + /; + + @{$glyphs{"þ"}} = qw/ + _____ + X____ + XXX__ + X__X_ + X__X_ + XXX__ + X____ + _____ + /; + + @{$glyphs{"ÿ"}} = qw/ + ______ + _X_X__ + ______ + X___X_ + X___X_ + _X_X__ + __X___ + _X____ + /; + + # end of Latin-1. a few misc emoji glyphs here. + + @{$glyphs{"☺"}} = qw/ + _XXXXXXX__ + X_______X_ + X_XX_XX_X_ + X_______X_ + X_X___X_X_ + X__XXX__X_ + X_______X_ + _XXXXXXX__ + /; + + @{$glyphs{"☹"}} = qw/ + _XXXXXXX__ + X_______X_ + X_XX_XX_X_ + X_______X_ + X__XXX__X_ + X_X___X_X_ + X_______X_ + _XXXXXXX__ + /; + +## @{$glyphs{"☺"}} = qw/ +## ________ +## _XXXXX__ +## X__X__X_ +## XXXXXXX_ +## X_XXX_X_ +## XX___XX_ +## _XXXXX__ +## ________ +## /; +## + + @{$glyphs{"☮"}} = qw/ + _XXXXXXX__ + X___X___X_ + X___X___X_ + X__XXX__X_ + X_X_X_X_X_ + XX__X__XX_ + X___X___X_ + _XXXXXXX__ + /; + +## @{$glyphs{"☮"}} = qw/ +## ________ +## _XXXXX__ +## X__X__X_ +## X_XXX_X_ +## XX_X_XX_ +## X__X__X_ +## _XXXXX__ +## ________ +## /; + + @{$glyphs{"☯"}} = qw/ + _XXXXXXXX__ + XXXXXX___X_ + XX_XX____X_ + XXXX_____X_ + XXXXX____X_ + XXXX___X_X_ + XXX______X_ + _XXXXXXXX__ + /; + + for my $k (sort keys %glyphs) { + my @g = @{$glyphs{$k}}; + + # this is needed to print $k correctly in irssi. + if(__PACKAGE__ ne 'main') { + utf8::encode($k); + } + + my $width = length($g[0]); + for(@g) { + if(/([^X_])/) { + die "glyph for $k has invalid definition char '$1' (not X or _)!"; + } + die "glyph for $k has inconsistent width!" unless length == $width; + die "glyph for $k doesn't have blank rightmost column" if /X$/; + } + + if(@g != 8) { + die "glyph for $k not 8 lines tall!"; + } + } +} diff --git a/colors_per_channel.pl b/colors_per_channel.pl new file mode 100644 index 0000000..56ce127 --- /dev/null +++ b/colors_per_channel.pl @@ -0,0 +1,103 @@ +#!/usr/bin/perl + +# Quickstart: + +# /run colors_per_channel.pl +# ...or, copy/link to your ~/.irssi/scripts/autorun + +# To strip colors from public messages in #badchannel and #otherchannel, +# and from private messages from user badnick: + +# /set no_color_from #badchannel badnick #otherchannel + +# no_color_from is a space-separated list of channels and/or nicks +# you want to strip colors from. + +# use #channel, &channel, or nick (with no prefix) + +use warnings; +use strict; + +use Irssi qw/ + settings_add_str settings_add_bool settings_add_int + settings_get_str settings_get_bool settings_get_int + settings_set_str settings_set_bool settings_set_int + command command_bind command_unbind + signal_emit signal_add_last + timeout_add timeout_remove/; + +our $VERSION = '0.1'; +our %IRSSI = ( + authors => 'Urchlay', + contact => 'Urchlay on NewNet', + name => 'urlmanager', + description => 'Selectively strip colors in text from certain channels/nicks', + license => 'Same as Perl', + url => 'none', +); + +# pub_check_color dynamically changes the hide_colors setting, based on the +# nick/channel the message came from. +sub pub_check_color { + my (undef, undef, $nick, undef, $channel) = @_; + + # If there's no channel, it means we were called in response to a + # "message private" signal (so we should match the nick instead). + $channel = $nick if not $channel; + + # Splitting the setting up every time is sub-optimal: it would be + # better to do it once at script load and on signal "setup changed", + # but this way is easier to code and understand... and split() isn't + # really all that slow. + my @list = split " ", settings_get_str('no_color_from'); + my $hide = grep { $channel =~ /$_/i } @list; + + # only change setting if it should be changed. An optimization, since + # who knows how many other scripts are loaded and listening to the + # "setup changed" signal... + if($hide != settings_get_bool('hide_colors')) { + settings_set_bool('hide_colors', $hide); + signal_emit('setup changed'); + } +} + +# Thanks to tarbo on freenode/#irssi for the idea behind this bit... +# It allows us to save the original value of hide_colors at script load, +# and restore it at script unload. I've commented it in "tutorial mode" +# because I think it's an important technique for irssi scripts. + +# Note to scripters reading this: I'm only saving/restoring one value, +# so $old_value is a scalar... but it could just as well be a +# hash or an array, if you need to save/restore multiple settings. +# Alternately, you could use multiple scalar values... though in that +# case, you do NOT need to bless them all in step 3 (just pick one +# and bless only it). + +# Step 1: save old setting (happens at script load) in a variable with +# file scope (aka an "our" variable) +our $old_value = settings_get_bool('hide_colors'); + +# Step 2: define DESTROY sub to restore the old setting. Actually this +# is an object method that perl automatically calls on any object when it +# goes out of scope (see "perldoc perltoot"). If your script does things +# like open sockets/files, you could close them in DESTROY as well. +sub DESTROY { + settings_set_bool('hide_colors', $old_value); + signal_emit('setup changed'); +} + +# Step 3: bless the setting into the current package. Doing this will +# cause irssi to call the DESTROY sub (method, actually) when $old_value +# goes out of scope... which will happen at script unload! +# Remember, if you're saving/restoring multiple values in their own +# scalars, you only bless ONE of them (otherwise, DESTROY() will get +# called multiple times). +# An interesting philosophical question: does the use of "bless" mean +# this script is object-oriented or not? Discuss :) +bless \$old_value, __PACKAGE__; + +# Rest of script is standard irssi stuff... +settings_add_str('colors_per_channel', 'no_color_from', ''); +signal_add_last('message public', \&pub_check_color); +signal_add_last('message private', \&pub_check_color); + diff --git a/complete_text.pl b/complete_text.pl new file mode 100644 index 0000000..bac2fa0 --- /dev/null +++ b/complete_text.pl @@ -0,0 +1,142 @@ +#!/usr/bin/perl + +# script was written ages ago, sometime around 2008. + +use warnings; +use strict; + +use Irssi qw{ + signal_add_last signal_add + command_bind + settings_get_int settings_add_int + settings_get_str settings_add_str +}; + +our $VERSION = "0.1"; +our %IRSSI = ( + authors => 'Urchlay', + contact => 'Urchlay on NewNet', + name => 'complete_text', + description => 'Create a dictionary from channel/msg text, ' . + 'use for word completion', + license => 'Same as Perl', + url => 'none', +); + +# TODO: +# - allow user to save the dictionary and reload +# - allow user to remove words from dictionary +# - better case-matching: store words in their original case or lowercase +# (user setting). If $word is initial cap, complete to full original +# case (so in #atari, A<tab> would be AtariSIO, not Atarisio) +# - add channel topic and maybe /quit msg +# - maybe expire old words based on use frequency? +# - typo support like irccomplete? + +settings_add_int('complete_text', 'complete_dict_size', 5000); +settings_add_str('complete_text', 'complete_preload', "/usr/dict/words"); + +our $limit = settings_get_int('complete_dict_size'); +our %seen; +our @dict; +our %static_seen; +our @static_dict; + +sub complete_word { + my ($complist, $window, $word, $linestart, $want_space) = @_; + + my $prefix = $1 if $word =~ s/^(.*\b)(\w)/$2/; + my $initial_cap = ($word =~ /^[A-Z]/); + + $word = quotemeta $word; # 20080723 bkw: d'oh! + push @$complist, map { $_ = $prefix . $_ } grep { $_ =~ /^$word/i } @dict; + push @$complist, map { $_ = $prefix . $_ } grep { $_ =~ /^$word/i } @static_dict; + + if($initial_cap) { + s/^(.)/uc($1)/e for @$complist; + } +} + +sub add_to_dict { + for(lc($_[1]) =~ /(\w{4,})/g) { + s/^_(\w+)_$/$1/; + s/^\*(\w+)\*$/$1/; + + next if $static_seen{$_}; + next if $seen{$_}++; + + push @dict, $_; + if(@dict >= $limit) { + my $old = shift @dict; + delete $seen{$old}; + } + } +} + +sub dumpdict { + print "dynamic dictionary"; + print for @dict; + print scalar keys %seen; + print scalar @dict; + print "\nstatic dictionary"; +# print for sort keys %static_seen; + print scalar keys %static_seen; +} + +sub load_static_seen { + %static_seen = (); + @static_dict = (); + + my @files = split /:/, settings_get_str('complete_preload'); + + for(@files) { + print "preloading words from $_"; + s/^~/$ENV{HOME}/; + print "$_ not found, skipping", next unless -f $_; + + open my $f, "<$_" or do { print "$_: $!"; next; }; + while(<$f>) { + chomp; + next unless $_; + $static_seen{$_}++; + } + + close $f; + + @static_dict = sort { + (length $a <=> length $b) || ($a cmp $b) + } keys %static_seen; + } + + print "preloaded " . (scalar keys %static_seen) . " words"; +} + +sub setup_changed { + my $new_limit = settings_get_int('complete_dict_size'); + + if($new_limit < @dict) { + #print "deleting " . (@dict - $new_limit) . " elements"; + for(my $i=0; $i < (@dict - $new_limit); $i++) { + my $old = shift @dict; + delete $seen{$old}; + } + } + + $limit = $new_limit; + + load_static_seen(); +} + +load_static_seen(); +signal_add_last('setup changed', \&setup_changed); +signal_add_last('complete word', \&complete_word); +command_bind('dumpdict', \&dumpdict); + +for( + "message public", "message private", + "message own_public", "message own_private", + "message dcc", "message dcc own", + "message dcc own_action", "message dcc action") +{ + signal_add_last($_, \&add_to_dict); +} diff --git a/frotzglue.pl b/frotzglue.pl new file mode 100644 index 0000000..0105fe9 --- /dev/null +++ b/frotzglue.pl @@ -0,0 +1,445 @@ +# convert this to a man page with: +# pod2man --stderr -s6 -c"Irssi Scripts" -r0.1 -u frotzglue.pl + +# for HTML: +# pod2html frotzglue.pl + +=pod + +=head1 NAME + +frotzglue.pl - run a z-code interactive fiction game within irssi, in a channel. + +=head1 SYNOPSIS + +/script load frotzglue.pl + +=head1 DESCRIPTION + +B<frotzglue.pl> is a Perl script for irssi(1) which runs a z-code game +using dfrotz(6), and allow users in an IRC channel to co-operatively +play the game. + +You probably don't want to run this in your regular irssi session that +you use for normal chatting. It's basically a bot, intended to be run +with a dedicated irssi instance that uses its own separate user, or at +least a separate config dir (irssi's B<--home> option). + +=head2 Gameplay + +When a game is started, a list of players (IRC nicks) must be given. These +players will take turns sending a command to the game. + +All output from the game is sent to the IRC channel where the game +was started. When the game is ready for input, there will be a prompt +with the current player's nick (whoever's turn it is) followed by a > +character. + +Due to irssi's anti-flooding mechanism (and/or the IRC server's), +output will be slow, with pauses between lines. This means that some +games (ones that rely on timed prompts) might be unplayable. + +=head2 Commands + +Only public (channel) messages are accepted; there's no way to play or +control a game via private messages. + +All commands are prefixed with B<.if>; any other channel text is ignored. + +=head3 Admin Commands + +These commands can be executed by anyone (TODO: for now; later there will be some +sort of access control). + +=over 4 + +=item B<.if lsgames> + +Show the list of supported games. These are hard-coded in the script. + +TODO: store the game list as an irssi setting. + +=item B<.if newgame> I<game> [I<player> I<...>] + +Start a game. I<game> must be one of the supported games. Any number +of players may be given. If there are no players, a 1-player game is +started, with the only player being the user who started the game. + +=item B<.if status> + +Tell whether or not a game is running. If it is, show the game name, channel, +list of players, and whose turn it is. + +=item B<.if endgame> + +Ends any game in progress. + +=back + +=head3 Game Control Commands + +These commands only work while a game is in progress, and can only be given +by players. + +=over 4 + +=item B<.if skipturn> + +Skip (pass) the current user's turn. Use this to avoid holding up gameplay +if someone is AFK or has gotten disconnected. + +=item B<.if add> I<player> [I<player> I<...>] + +Adds one or more players to the game. + +=item B<.if rm> I<player> [I<player> I<...>] + +Removes one or more players from the game. If there's only one player +on the list, attempts to remove him will be denied; the game must +always have at least one player. + +=item B<.if ls> + +List the players in the game, and tell whose turn it is. (Note: possibly this +shouldn't exist; .if status shows the same info...) + +=back + +=head3 Gameplay Commands + +Any message beginning with B<.if> that isn't one of the recognized commands +above will be sent to the game as input. In a multi-player game, only the +player whose turn it is can send commands to the game. + +Examples might be B<.if get sword> or B<.if inventory>. + +To send a blank line to the game, say B<.if \n> or just B<.if> by itself. + +To send one of the listed admin or control commands to the game instead of +acting on it, prefix it with B<-->. Example: B<.if -- ls> would send the +command "ls" to the game (where it would probably result in an error, +such as B<I don't know the word "ls">). + +Currently, the B<save> and B<load> commands are disabled (for security +reasons). At some point, there might be a safe way to implement these. + +=head1 EXAMPLES + +[put an example here] + +=head1 BUGS + +All game text is assumed to end with a > prompt. If something the game +says doesn't end with >, there will be a 5-second timeout before the +text will appear... and the text will be printed again, next time a > +prompt is encountered. This is because of the author's inexperience +with the perl Expect module, and will likely be fixed soon. + +Use of the dfrotz \ commands (list under CONFIGURATION in its man +page) is allowed, but probably shouldn't be. It's probably possible to +break the Expect stuff with some of the \ commands. + +Only one game can be running at a time... not "one game per IRC +channel", just one, period. There should be a way to run separate +games in different channels. + +There's a $VERBOSE variable for debugging, but it doesn't do much, and +the script has to be edited and reloaded to set/unset it. + +=head1 AUTHOR + +B. Watson <urchlay@slackware.uk>, AKA Urchlay on the Libera IRC network. + +=cut + + +# Requirements: +# - irssi (of course). +# - dfrotz somewhere on $PATH. +# - Expect perl module. + +# the game has a defined list of players, and on every move, +# the next player has control. +# commands are prefixed with .if: + +# These commands work any time, and can be run by anyone (for now). + +# .if lsgames +# - list all the games we know about. + +# .if newgame <game> <player> <...> +# - start a new game. + +# .if status +# - tell whether or not a game is running. if so, list filename, channels, and players. + +# .if endgame +# - end any game in progress (and kill its dfrotz process). + +# These commands only work while a game is in progress, and can only +# be issued by players: + +# .if skipturn +# - pass the current player's turn. use if someone's AFK or disconnected. + +# .if add <player> +# .if rm <player> +# .if ls +# - add/delete/list players. + +# anything else beginning with .if is passed to the game as-is. +# if you *really* want to pass one of the above commands as game input, +# use e.g. ".if -- whatever". + +use strict; +use vars qw($VERSION %IRSSI $VERBOSE $exp @players $cur_player %games $gameprefix $cur_game $gamechan); + +use Irssi qw(signal_add); + +$VERSION = '0.1'; +%IRSSI = ( + authors => 'B. Watson', + contact => 'urchlay@slackware.uk', + name => 'frotzglue', + description => 'Run a z-code interactive fiction game in a channel', + license => 'WTFPL', +); + +$VERBOSE = 1; # set to 1 if needed. + +use Expect; +$Expect::Log_Stdout = 0; + +$gameprefix = "/usr/share/zcode/"; +%games = ( + zork1 => [ "Zork I", $gameprefix . "zork1.z3" ], + zork2 => [ "Zork II", $gameprefix . "zork2.z3" ], + zork3 => [ "Zork III", $gameprefix . "zork3.z3" ], + hhgg => [ "Hitchhiker's Guide", $gameprefix . "hhgg.z3" ], +); + +sub player_prompt { + my ($server, $target) = @_; + return unless $exp; + $server->command("msg $target " . current_player() . ">"); +} + +sub end_game { + $exp->hard_close(); + undef $exp; + undef $cur_game; + undef $gamechan; + undef @players; + undef $cur_player; +} + +sub run_game { + my ($server, $target) = @_; + $exp->expect(10, '>'); + + my $xstat = $exp->exitstatus(); + + if(defined($xstat)) { + my $normal = $xstat ? "due to error" : "normally"; + $server->command("msg $target *** game exited $normal with status $xstat."); + end_game(); + return; + } + +## my $err = $exp->error(); +## if($err) { +## $server->command("msg $target *** got error $err, aborting game."); +## end_game(); +## return; +## } + + for(split /\n/, $exp->before()) { + s/\r//g; + $_ = " " unless length; + $server->command("msg $target $_"); + } +} + +sub cant_spawn { + my ($server, $target) = @_; + $server->command("msg $target *** game failed to execute!"); +} + +sub spawn_game { + return unless $cur_game; + my $gamepath = $games{$cur_game}[1]; + return unless $gamepath; + $exp = Expect->new; + if(!$exp->spawn("dfrotz", "-q", "-m", "-f", "irc", $gamepath)) { + cant_spawn(@_); + end_game(); + return; + } + run_game(@_); + player_prompt(@_); +} + +sub next_player { + return unless @players; + $cur_player++; + $cur_player %= @players; +} + +sub is_playing { + return 0 unless @players; + grep { $_ eq $_[0] } @players; +} + +sub current_player { + return 0 unless @players; + return $players[$cur_player]; +} + +sub player_list { + return "" unless @players; + return join(", ", @players); +} + +sub on_message { + my ($server, $msg, $nick, $address, $target) = @_; + my $mynick = $server->{nick}; + + $msg = '.if \n' if $msg =~ /^\.if\s*$/; + + return unless $msg =~ s/^\.if\s+//; + return unless length $msg; + + warn "nick='$nick', address='$address', target='$target', mynick='$mynick'" if $VERBOSE; + + (my $cmd, my @args) = split " ", $msg; + $cmd = lc $cmd; + + warn "cmd='$cmd'" if $VERBOSE; + + # all responses start with this prefix. + my $resp = "msg $target $nick"; + + if($cmd eq 'newgame') { + if(!@args) { + $server->command("$resp: newgame requires at least a game name."); + return; + } + + my $game = shift @args; + + if(!@args) { + #$server->command("$resp: newgame requires at least one player."); + #return; + push @args, $nick; + } + + if($exp) { + $server->command("$resp: a game of $cur_game is already in progress in $gamechan; use 'endgame' to end it."); + } else { + if(!$games{$game}) { + $server->command("$resp: no such game as $game; try '.if lsgames'."); + return; + } + $server->command("$resp: starting a " . @args . "-player game of $game in $target."); + $cur_game = $game; + @players = @args; + $cur_player = 0; + $gamechan = $target; + spawn_game($server, $target); + } + return; + } elsif($cmd eq 'help') { + $server->command("$resp: Usage: .if [newgame <players>|status|endgame|lsgames|skipturn|add|ls|rm|<game command>]"); + return; + } elsif($cmd eq 'status') { + if($exp) { + $server->command("$resp: game '$cur_game' is running on $gamechan, with players " . player_list() . "; " . current_player() . "'s turn."); + } else { + $server->command("$resp: game is NOT running."); + } + return; + } elsif($cmd eq 'endgame') { + if($exp) { + $server->command("$resp: ending game."); + end_game(); + } else { + $server->command("$resp: can't end; game is NOT running."); + } + return; + } elsif($cmd eq 'lsgames') { + $server->command("$resp: I know how to play these games:"); + for(sort keys %games) { + $server->command("$resp: $_ ($games{$_}[0])"); + } + return; + } + + if(!$exp) { + $server->command("$resp: '$cmd' not valid unless game is running."); + return; + } + + if($target ne $gamechan) { + $server->command("$resp: wrong channel; game is running in $gamechan."); + return; + } + + if(!is_playing($nick)) { + $server->command("$resp: $nick: you are not a player in the game."); + return; + } + + if($cmd eq 'skipturn') { + $server->command("$resp: skipping " . current_player() . "'s turn."); + next_player(); + player_prompt(); + return; + } elsif($cmd eq 'add') { + for(@args) { + if(is_playing($_)) { + $server->command("$resp: can't add $_: already playing."); + } else { + push @players, $_; + $server->command("$resp: added player: $_"); + } + } + return; + } elsif($cmd eq 'rm') { + for(@args) { + if(is_playing($_)) { + if(@players == 1) { + $server->command("$resp: cannot remove last player from game."); + return; + } + my $p = $_; + @players = grep { $_ ne $p } @players; + $cur_player %= @players; + $server->command("$resp: removed $_."); + } else { + $server->command("$resp: can't remove $_ (not playing)."); + } + } + return; + } elsif($cmd eq 'ls') { + $server->command("$resp: " . player_list()); + return; + } elsif($cmd eq 'save' || $cmd eq 'load') { + $server->command("$resp: save and load commands are disabled."); + return; + } else { + if(current_player() eq $nick) { + $msg =~ s/^\s*--\s*//; + $server->command("$resp: sending '$msg' to game."); + $msg = "" if $msg eq '\n'; + $exp->send($msg . "\n"); + run_game($server, $target); + next_player(); + player_prompt($server, $target); + } else { + $server->command("$resp: it's not your turn; it's " . current_player() . "'s."); + return; + } + } +} + +signal_add('message public', 'on_message'); diff --git a/help_path_completion.pl b/help_path_completion.pl new file mode 100644 index 0000000..8bb5e6a --- /dev/null +++ b/help_path_completion.pl @@ -0,0 +1,43 @@ +#!/usr/bin/perl + +use warnings; +use strict; + +use Irssi qw/settings_get_str settings_set_str signal_emit signal_add_last/; + +our $VERSION = "0.1"; +our %IRSSI = ( + authors => 'Urchlay', + contact => 'Urchlay on NewNet', + name => 'help_path_completion', + description => 'Support /help word completion for files in help_path', + license => 'Same as Perl', + url => 'none', +); + +sub complete_word { + my ($complist, $window, $word, $linestart, $want_space) = @_; + return unless $linestart =~ m{/help}i; + + for(split /:/, settings_get_str('help_path')) { + opendir my $dir, $_ or next; + push @$complist, grep { $_ !~ /\./ && $_ =~ /$word/i } readdir($dir); + closedir $dir; + } +} + +# Add per-user help dir to help_path, if not already present. +sub init_help_path { + my $dir = "$ENV{HOME}/.irssi/help"; + my $help_path = settings_get_str('help_path'); + + return if grep { $_ eq $dir } split /:/, $help_path; + + $help_path .= ":$dir"; + settings_set_str('help_path', $help_path); + + signal_emit('setup changed'); +} + +init_help_path(); +signal_add_last("complete word", \&complete_word); diff --git a/jumble.pl b/jumble.pl new file mode 100644 index 0000000..79ac4fa --- /dev/null +++ b/jumble.pl @@ -0,0 +1,45 @@ +#!/usr/bin/perl + +# script was written ages ago (probably 2008 or earlier). + +use warnings; +use strict; + +use Irssi qw/command command_bind/; + +our $VERSION = "0.1"; +our %IRSSI = ( + authors => 'Urchlay', + contact => 'Urchlay on NewNet', + name => 'jumble', + description => 'Smiultae bad tpying', + license => 'Same as Perl', + url => 'none', +); + +sub jumble { + my ($text, $srv, $chan) = @_; + my @words = split /\s+/, $text; + + for(@words) { + if(length($_) > 2) { + /(\W*\w)(\w+)(\w\W*)/; + if($2) { + my $start = $1; + my $middle = $2; + my $end = $3; + + my @letters = split "", $middle; + for(0..$#letters-1) { + ($letters[$_], $letters[$_+1]) = ($letters[$_+1], $letters[$_]) + if rand > 0.5; + } + $_ = $start . join("", @letters) . $end; + } + } + } + + $chan->command('MSG ' . $chan->{name} . ' ' . join(" ", @words)); +} + +command_bind("jumble", \&jumble); diff --git a/newdice.pl b/newdice.pl new file mode 100644 index 0000000..e1f9f80 --- /dev/null +++ b/newdice.pl @@ -0,0 +1,205 @@ + +# newdice.pl - AD&D dice roller for IRC. +# Loosely based on 'dice_concise.pl', here: +# https://scripts.irssi.org/scripts/dice_concise.pl +# ...but completely rewritten, so I don't feel bound by the GPL. + +# Differences between this and dice_concise.pl: + +# - The count can be omitted, so "d6" means "1d6". +# - If the count is omitted, the "d" can also be omitted, so "6" means "1d6". +# - Multiple arguments are supported, e.g. "!roll 1d20 1d8+4", up to $max_args. +# - A repeat count can be given as the first argument, e.g. "!roll 6x 3d6" +# for rolling stats. +# - The command prefix can either be ! or the bot's nick. +# - Private messages are supported, with or without the command prefix. +# - Modifiers are eval'ed as perl code, so they can be arbitrarily complex, +# and support proper precedence (+2*20 means *40). +# - Add !reroll command. doesn't track context, literally just rerolls +# the last thing that was rolled, no matter who did it or what channel +# or privmsg it was. +# - Simplify error messages. parse errors just print the help, there's no +# need for a separate help command. +# - Got rid of the German translations. I had to write new error messages, +# and I don't speak German. The "2w6" notation is still supported. +# - Change the presentation of the output (remove commas, for one thing). also, +# don't print the raw roll in brackets if it's a plain 1d roll with no +# modifier (for 1d6, you just get 6, not "6 [6]"). +# - Got rid of limit of 100 sides. I can't see much use for 1000+ sided dice, +# but it does no harm to support them. + +use strict; +use vars qw($VERSION %IRSSI $VERBOSE $parse_regex $max_dice $max_args $lastmsg); + +use Irssi qw(signal_add); + +$VERSION = '0.1'; +%IRSSI = ( + authors => 'B. Watson', + contact => 'urchlay@slackware.uk', + name => 'newdice', + description => 'Die roller that accepts AD&D notation', + license => 'WTFPL', +); + +$VERBOSE = 0; # set to 1 if needed. + +$max_dice = 20; +$max_args = 6; + +# I tried to make this regex as readable as possible. It doesn't +# handle the 'relaxed' syntax where the count and/or the letter d +# are missing: those get added by other code, so this will always see +# e.g. "1d6" even if "6" was the original input. +$parse_regex = qr/ + ^ # anchor at start of arg (to avoid e.g. -1d6). + (\d+) # count of dice (required). + d # the letter d or D (required). + (\d+) # number of sides on die (required). + ( # modifier(s)... + [-+\/*] # first modifier *must* start with arithmetic operator. + [-+\/*\d]+ # the rest is mix-and match, arith ops and digits. + )? # ...modifiers are optional. + $ # avoid trailing junk, e.g. "1d6foo". +/ix; + +# This always rounds up if the fractional part is >= 0.5, regardless +# of sign (5.5 rounds to 6, -5.5 rounds to -5). Could have used +# POSIX::ceil, but POSIX is a lot of overhead just for one simple +# funciton. +sub round_up(\$) { + my $v = ${$_[0]}; + my $i = int(${$_[0]}); + my $sgn = ($v < 0 ? -1 : 1); + $i += $sgn if ($v - $i >= 0.5); + ${$_[0]} = $i; +} + +sub on_message { + my ($server, $msg, $nick, $address, $target) = @_; + my $mynick = $server->{nick}; + + unless(length $target) { + $target = $nick; + $nick = $mynick; + } + + if($target eq $mynick) { + # private message... send response to sender + $target = $nick; + $msg =~ s/^/!/; + } + + # all responses start with this prefix. + my $resp = "msg $target $nick"; + + # command prefix can be ! or nick followed by an optional punctuation mark + # and a required space. + $msg =~ s/^$mynick[^\sA-Za-z0-9]\s/!/; + + # allow multiple ! (makes the logic simpler) + return unless $msg =~ /^!+(re)?roll(?:$|\s*(\S.*)?$)/i; + + if($1) { # re-rolling last args... + if(defined($2)) { + $server->command("$resp: Usage: !reroll (no arguments)."); + return; + } elsif(defined $lastmsg) { + $msg = $lastmsg; + } else { + $server->command("$resp: can't re-roll, nobody has rolled yet."); + return; + } + } + + warn "\$nick is $nick, \$target is $target, server nick is " . $server->{nick} if $VERBOSE; + + # accept d% as a synonym for d100 + $msg =~ s/%/100/g; + + # the 'undef' gets rid of the command (the !roll). + (undef, my @args) = split " ", $msg; + + if($args[0] && ($args[0] =~ /^([1-9])x$/)) { + shift @args; + my @newargs; + push @newargs, @args for 1..$1; + @args = @newargs; + } + + warn join(", ", @args) if $VERBOSE; + + if(@args > $max_args) { + $server->command("$resp: too many arguments (limit is $max_args)"); + return; + } elsif(!@args) { + push @args, "help"; + } + + for my $arg (@args) { + # support the German "w" notation, for compatibility with dice_concise.pl. + if($arg =~ /^\d*w/i) { + $arg =~ s/w/d/i; + } + + # add default prefixes (count and the letter 'd') if missing. + # allows e.g. "!roll 6" or "!roll 1d6" to mean "!roll 1d6", + # or even "!roll 6+2" to mean "!roll 1d6+2". + $arg =~ s/^/d/ if $arg !~ /d/i; + $arg =~ s/^/1/ if $arg =~ /^d/i; + + if($arg =~ $parse_regex) { + my $count = $1 || 1; + my $sides = $2; + my $mods = $3 || ""; + my $canonical = "${count}d${sides}${mods}"; + my @rolls; + my $total = 0; + + if($count > $max_dice) { + $server->command("$resp: $canonical: too many dice (limit is $max_dice)"); + next; + } + + if($sides < 2) { + $server->command("$resp: $canonical: not enough sides (must be at least 2)"); + next; + } + + for(1..$count) { + my $got = int(rand($sides) + 1); + $total += $got; + push @rolls, $got; + } + + # modifiers can be anything that's valid perl. this isn't a + # security hole because the $parse_regex only accepts + - * / + # and digits (so, no way to execute arbitrary code). + eval "\$total = $total $mods;" if $mods; + + if($@) { + $server->command("$resp: $canonical: Invalid modifier '$mods'."); + } else { + round_up($total); + my $msg = "$resp rolls $canonical and gets: $total"; + if(($count > 1) || $mods) { + $msg .= " [" . join(' ', @rolls) . "]"; + } + $server->command($msg); + } + } else { + $server->command($resp . ': Usage: !roll [<repeat>x] [<numdice>][d]<sides>[<+-*/>modifier(s)] ... (e.g. "!roll 2d20", "!roll 2d20*2+10"'); + return; + }; + + } + $lastmsg = $msg; + return; +} + +signal_add($_, 'on_message') for ( + 'message private', + 'message own_private', + 'message public', + 'message own_public', +); @@ -0,0 +1,1201 @@ +### modified version by Urchlay: +### translate remaining Polish prompts to English. + +use strict; +use 5.005_62; # for 'our' +use Irssi 20020428; # for Irssi::signal_continue +use vars qw($VERSION %IRSSI); + +$VERSION = "1.8"; +%IRSSI = ( + authors => 'Marcin \'Qrczak\' Kowalczyk', + contact => 'qrczak@knm.org.pl', + name => 'Seen', + description => 'Tell people when other people were online', + license => 'GPL', + url => 'http://qrnik.knm.org.pl/~qrczak/irssi/seen.pl', +); + +######## User interface ######## + +# COMMANDS +# ======== +# +# /seen <nick> +# Show last seen info about nick. +# +# /say_seen [<to_whom>] <nick> +# Say last seen info about nick in the current window. If to_whom +# is present, answer as if that person issued a seen request. +# +# /listen on [[<chatnet>] <channel>] +# Turn on listening for seen requests in the current or given channel. +# +# /listen off [[<chatnet>] <channel>] +# Turn off listening for seen requests in the current or given channel. +# +# /listen delay [[<chatnet>] <channel>] +# Turn on listening for seen requests in the current or given channel. +# We will reply only if nobody else replies with a message containing +# the given nick (probably a seen reply from another bot) in seen_delay +# seconds. +# +# /listen private [[<chatnet>] <channel>] +# Turn on listening for seen requests in the current or given channel. +# The reply will be sent as a private notice. +# +# /listen disable [[<chatnet>] <channel>] +# Same as "off", used to distinguish channels where we won't listen +# for sure from channels we didn't specify anything about. +# +# /listen list +# Show which channels we are listening for seen requests on. + +# Forms of seen requests from other people: +# Public message "<our_nick>: seen <nick>". +# Public message "seen <nick>" on channels where we are listening. +# Private message "seen <nick>". +# Any of the above with "!seen" instead of "seen". +# Any of the above with a question mark at the end. +# Any of the above with "jest <nick>?", "by <nick>?", "bya <nick>?", +# "<nick> jest?", "<nick> by?", "<nick> bya?", with optional +# "czy" at the beginning - provided that we know that nick +# (to avoid treating some other message as a seen request). + +# VARIABLES +# ========= +# +# seen_expire_after +# After that number of days we forget about nicks and addresses. +# Default 30. +# +# seen_expire_asked_after +# After that number of days we forget that that somebody was +# searched for and don't send a notice. Default 7. +# +# seen_delay +# On channels set to '/listen delay' we reply if after that number +# of seconds nobody else replies. Default 60. + +######## Internal structure of the database in memory ######## + +# %listen_on = (chatnet => {channel => listening}) +# %address_absent = (chatnet => {address => time}) +# %nicks = (chatnet => {address => [nick]}) +# %last_nicks = (chatnet => {address => nick}) +# %how_quit = (chatnet => {address => how_quit}) +# %spoke = (chatnet => {address => time}) +# %nick_absent = (chatnet => {nick => time}) +# %addresses = (chatnet => {nick => address}) +# %orig_nick = (chatnet => {nick => nick}) +# %channels = (chatnet => {nick => [channel]}) +# %asked = (chatnet => {nick => {nick_asks => time}}) + +# listening: +# 'on', undef = 'off', 'delay', 'private', 'disable' + +# how_quit: +# ['disappeared'] +# ['was_left', kanal] +# ['left', channel, reason] +# ['quit', channels, reason] +# ['was_kicked', channel, kicker, reason] + +######## Global variables ######## + +our %listen_on = (); +our %address_absent = (); +our %nicks = (); +our %last_nicks = (); +our %how_quit = (); +our %spoke = (); +our %nick_absent = (); +our %addresses = (); +our %orig_nick = (); +our %channels = (); +our %asked = (); + +Irssi::settings_add_int "seen", "seen_expire_after", 30; # days +Irssi::settings_add_int "seen", "seen_expire_asked_after", 7; # days +Irssi::settings_add_int "seen", "seen_delay", 60; # seconds + +our $database = Irssi::get_irssi_dir . "/seen.dat"; +our $database_tmp = Irssi::get_irssi_dir . "/seen.tmp"; +our $database_old = Irssi::get_irssi_dir . "/seen.dat~"; + +######## Utilities ######## + +our $nick_regexp = qr/ + [A-Z\[\\\]^_`a-z{|}\200-\377] + [\-0-9A-Z\[\\\]^_`a-z{|}\200-\377]* + /x; +our $seen_regexp = qr/^ *!?seen +($nick_regexp) *\?* *$/i; +our $maybe_seen_regexp1 = qr/ + ^\ * + (?:a\ +)? + (?:(?:if|when|here)\ +)? + (?:(?:dzi[s]|today|last time|recently|ju[z]|here|tutaj|mo[z]e)\ +)* + (?:in|by[l]a?)\ + + (?:(?:dzi[s]|today|last time|recently|ju[z]|here|tutaj|mo[z]e)\ +)* + ($nick_regexp) + (?:\ +(?:dzi[s]|today|last time|recently|ju[z]|here|tutaj|mo[z]e))* + \ *\?+\ *$/ix; +our $maybe_seen_regexp2 = qr/ + ^\ * + (?:a\ +)? + (?:(?:czy|kiedy|gdzie)\ +)? + (?:(?:dzi[s]|today|last time|recently|ju[z]|here|tutaj|mo[z]e)\ +)* + ($nick_regexp)?\ + + (?:(?:dzi[s]|today|last time|recently|ju[z]|here|tutaj|mo[z]e)\ +)* + (?:in|by[l]a?) + (?:\ +(?:dzi[s]|today|last time|recently|ju[z]|here|tutaj|mo[z]e))* + \ *\?+\ *$/ix; +our $exclude_regexp = qr/^(?:kto[s]?|who?|that?|that|ladna|i|a)$/i; + +sub lc_irc($) { + my ($str) = @_; + $str =~ tr/A-Z[\\]/a-z{|}/; + return $str; +} + +sub uc_irc($) { + my ($str) = @_; + $str =~ tr/a-z{|}/A-Z[\\]/; + return $str; +} + +our %lc_regexps = (); + +sub lc_irc_regexp($) { + my ($str) = @_; + $str =~ s/(.)/my $lc = lc_irc $1; my $uc = uc_irc $1; "[\Q$lc$uc\E]"/eg; + return $str; +} + +sub canonical($) { + my ($address) = @_; + $address =~ s/^[\^~+=-]//; + return $address; +} + +sub show_list(@) { + @_ == 0 and return ""; + @_ == 1 and return $_[0]; + return join(", ", @_[0..$#_-1]) . " i " . $_[$#_]; +} + +sub show_time_since($) { + my ($time) = @_; + my $diff = time() - $time; + $diff >= 0 or return "I don't know when (my watch is broken)"; + my $s = $diff % 60; $diff = int(($diff - $s) / 60); + my $m = $diff % 60; $diff = int(($diff - $m) / 60); + my $h = $diff % 24; $diff = int(($diff - $h) / 24); + my $d = $diff; + my $s_txt = $s ? "${s}s " : ""; + my $m_txt = $m ? "${m}m " : ""; + my $h_txt = $h ? "${h}h " : ""; + my $d_txt = $d ? "${d}d " : ""; + return + $d ? "$d_txt${h_txt}ago" : + $h ? "$h_txt${m_txt}ago" : + $m ? "$m_txt${s_txt}ago" : + "${s}s ago"; +} + +sub all_channels($@) { + my ($chatnet, @nicks) = @_; + my %chans = (); + foreach my $nick (@nicks) { + if ($channels{$chatnet}{lc_irc $nick}) { + foreach my $channel (@{$channels{$chatnet}{lc_irc $nick}}) { + $chans{$channel} = 1; + } + } + } + return keys %chans; +} + +sub is_private($) { + my ($channel) = @_; + return $channel && $channel->{mode} =~ /^[^ ]*[ps]/; +} + +sub mark_private($$) { + my ($channel, $name) = @_; + return is_private $channel ? "-$name" : $name; +} + +######## Actions on the database in memory ######## + +sub do_listen($$$) { + my ($chatnet, $channel, $state) = @_; + if ($state eq 'off') { + delete $listen_on{$chatnet}{$channel}; + } else { + $listen_on{$chatnet}{$channel} = $state; + } +} + +sub do_join($$$$) { + my ($chatnet, $address, $nick, $channel) = @_; + my $lc_nick = lc_irc $nick; + my $lc_channel = lc_irc $channel; + delete $address_absent{$chatnet}{$address}; + push @{$nicks{$chatnet}{$address}}, $nick + unless grep {lc_irc $_ eq $lc_nick} @{$nicks{$chatnet}{$address}}; + push @{$channels{$chatnet}{$lc_nick}}, $channel + unless grep {lc_irc $_ eq $lc_channel} @{$channels{$chatnet}{$lc_nick}}; + delete $how_quit{$chatnet}{$address}; + delete $nick_absent{$chatnet}{$lc_nick}; + $addresses{$chatnet}{$lc_nick} = $address; + $orig_nick{$chatnet}{$lc_nick} = $nick; +} + +sub do_quit_all($$$$$) { + my ($time, $chatnet, $address, $nick, $reason) = @_; + $address_absent{$chatnet}{$address} = $time; + delete $nicks{$chatnet}{$address}; + $last_nicks{$chatnet}{$address} = $nick; + $how_quit{$chatnet}{$address} = $reason; +} + +sub do_quit($$$$) { + my ($time, $chatnet, $address, $nick) = @_; + my $lc_nick = lc_irc $nick; + $nicks{$chatnet}{$address} = + [grep {lc_irc $_ ne $lc_nick} @{$nicks{$chatnet}{$address}}]; + delete $channels{$chatnet}{$lc_nick}; + $nick_absent{$chatnet}{$lc_nick} = $time; + $addresses{$chatnet}{$lc_nick} = $address; + $orig_nick{$chatnet}{$lc_nick} = $nick; +} + +sub do_part($$$$) { + my ($chatnet, $address, $nick, $channel) = @_; + my $lc_nick = lc_irc $nick; + my $lc_channel = lc_irc $channel; + $channels{$chatnet}{$lc_nick} = + [grep {lc_irc $_ ne $lc_channel} @{$channels{$chatnet}{$lc_nick}}]; +} + +sub do_nick($$$$$) { + my ($time, $chatnet, $address, $old_nick, $new_nick) = @_; + my $lc_old_nick = lc_irc $old_nick; + my $lc_new_nick = lc_irc $new_nick; + $nicks{$chatnet}{$address} = + [(grep {lc_irc $_ ne $lc_old_nick} @{$nicks{$chatnet}{$address}}), $new_nick]; + my $chans = $channels{$chatnet}{$lc_old_nick}; + delete $channels{$chatnet}{$lc_old_nick}; + $channels{$chatnet}{$lc_new_nick} = $chans; + $nick_absent{$chatnet}{$lc_old_nick} = $time; + delete $nick_absent{$chatnet}{$lc_new_nick}; + $addresses{$chatnet}{$lc_new_nick} = $address; + $orig_nick{$chatnet}{$lc_new_nick} = $new_nick; +} + +sub do_spoke($$$) { + my ($time, $chatnet, $address) = @_; + my $old_time = $spoke{$chatnet}{$address}; + $spoke{$chatnet}{$address} = $time + unless defined $old_time && $old_time > $time; +} + +sub do_ask($$$$) { + my ($time, $chatnet, $nick, $nick_asks) = @_; + my $lc_nick = lc_irc $nick; + my $lc_nick_asks = lc_irc $nick_asks; + my $old_time = $asked{$chatnet}{$lc_nick}{$lc_nick_asks}; + $asked{$chatnet}{$lc_nick}{$lc_nick_asks} = $time + unless defined $old_time && $old_time > $time; +} + +sub do_forget_ask($$$) { + my ($chatnet, $nick, $nick_asks) = @_; + my $lc_nick = lc_irc $nick; + my $lc_nick_asks = lc_irc $nick_asks; + delete $asked{$chatnet}{$lc_nick}{$lc_nick_asks}; +} + +######## Actions on the database in memory and in the file ######## + +sub append_to_database(@) { + open DATABASE, ">>$database"; + print DATABASE map {"$_\n"} @_; + close DATABASE; +} + +sub on_listen($$$) { + my ($chatnet, $channel, $state) = @_; + do_listen $chatnet, $channel, $state; + append_to_database "listen $state $chatnet $channel"; +} + +sub on_join($$$$) { + my ($chatnet, $address, $nick, $channel) = @_; + do_join $chatnet, $address, $nick, $channel; + append_to_database "join $chatnet $address $nick $channel"; +} + +sub on_quit_all($$$$) { + my ($chatnet, $address, $nick, $reason) = @_; + my $time = time(); + do_quit_all $time, $chatnet, $address, $nick, $reason; + append_to_database "quit_all $time $chatnet $address $nick @$reason"; +} + +sub on_quit($$$$) { + my ($chatnet, $address, $nick, $reason) = @_; + my $time = time(); + do_quit $time, $chatnet, $address, $nick; + append_to_database "quit $time $chatnet $address $nick"; + on_quit_all $chatnet, $address, $nick, $reason + unless @{$nicks{$chatnet}{$address}}; +} + +sub on_part($$$$$) { + my ($chatnet, $address, $nick, $channel, $reason) = @_; + do_part $chatnet, $address, $nick, $channel; + append_to_database "part $chatnet $address $nick $channel"; + on_quit $chatnet, $address, $nick, $reason + unless @{$channels{$chatnet}{lc_irc $nick}}; +} + +sub on_nick($$$$) { + my ($chatnet, $address, $old_nick, $new_nick) = @_; + my $time = time(); + do_nick $time, $chatnet, $address, $old_nick, $new_nick; + append_to_database "nick $time $chatnet $address $old_nick $new_nick"; +} + +sub on_spoke($$) { + my ($chatnet, $address) = @_; + my $time = time(); + return if $spoke{$chatnet}{$address} == $time; + do_spoke $time, $chatnet, $address; + append_to_database "spoke $time $chatnet $address"; +} + +sub on_ask($$$) { + my ($chatnet, $nick, $nick_asks) = @_; + my $time = time(); + do_ask $time, $chatnet, $nick, $nick_asks; + append_to_database "ask $time $chatnet $nick $nick_asks"; +} + +######## Reading the database from file ######## + +sub syntax_error() { + die "Syntax error in $database: $_"; +} + +our %parse_how_quit = ( + disappeared => sub { + return ['disappeared']; + }, + was_left => sub { + $_[0] =~ /^ ([^ ]*)$/ or syntax_error; + return ['was_left', $1]; + }, + left => sub { + $_[0] =~ /^ ([^ ]*) (.*)$/ or syntax_error; + return ['left', $1, $2]; + }, + quit => sub { + $_[0] =~ /^ ([^ ]*) (.*)$/ or syntax_error; + return ['quit', $1, $2]; + }, + was_kicked => sub { + $_[0] =~ /^ ([^ ]*) ([^ ]*) (.*)$/ or syntax_error; + return ['was_kicked', $1, $2, $3]; + }, +); + +sub parse_how_quit($) { + my ($how_quit) = @_; + $how_quit =~ /^([^ ]*)(| .*)$/ or syntax_error; + my $func = $parse_how_quit{$1} or syntax_error; + return $func->($2); +} + +our %parse_database = ( + listen => sub { + $_[0] =~ /^ (on|off|delay|private|disable) ([^ ]*) ([^ ]*)$/ or syntax_error; + do_listen $2, $3, $1; + }, + join => sub { + $_[0] =~ /^ ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*)$/ or syntax_error; + do_join $1, $2, $3, $4; + }, + quit_all => sub { + $_[0] =~ /^ ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) (.*)$/ or syntax_error; + my ($time, $chatnet, $address, $nick, $how_quit) = ($1, $2, $3, $4, $5); + do_quit_all $time, $chatnet, $address, $nick, parse_how_quit($how_quit); + }, + quit => sub { + $_[0] =~ /^ ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*)$/ or syntax_error; + do_quit $1, $2, $3, $4; + }, + part => sub { + $_[0] =~ /^ ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*)$/ or syntax_error; + do_part $1, $2, $3, $4; + }, + nick => sub { + $_[0] =~ /^ ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*)$/ or syntax_error; + do_nick $1, $2, $3, $4, $5; + }, + spoke => sub { + $_[0] =~ /^ ([^ ]*) ([^ ]*) ([^ ]*)$/ or syntax_error; + do_spoke $1, $2, $3; + }, + ask => sub { + $_[0] =~ /^ ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*)$/ or syntax_error; + do_ask $1, $2, $3, $4; + }, + forget_ask => sub { + $_[0] =~ /^ ([^ ]*) ([^ ]*) ([^ ]*)$/ or syntax_error; + do_forget_ask $1, $2, $3; + }, +); + +sub read_database() { + open DATABASE, $database or return; + while (<DATABASE>) { + chomp; + /^([^ ]*)(| .*)$/ or syntax_error; + my $func = $parse_database{$1} or syntax_error; + $func->($2); + } + close DATABASE; +} + +######## Writing the database to file ######## + +sub write_database { + open DATABASE, ">$database_tmp"; + foreach my $chatnet (keys %listen_on) { + foreach my $channel (keys %{$listen_on{$chatnet}}) { + my $state = $listen_on{$chatnet}{$channel}; + print DATABASE "listen $state $chatnet $channel\n"; + } + } + foreach my $chatnet (keys %nick_absent) { + foreach my $nick (keys %{$nick_absent{$chatnet}}) { + my $time = $nick_absent{$chatnet}{$nick}; + my $address = $addresses{$chatnet}{$nick}; + my $orig = $orig_nick{$chatnet}{$nick}; + print DATABASE "quit $time $chatnet $address $orig\n"; + } + } + foreach my $chatnet (keys %address_absent) { + foreach my $address (keys %{$address_absent{$chatnet}}) { + my $time = $address_absent{$chatnet}{$address}; + my $nick = $last_nicks{$chatnet}{$address}; + my $reason = $how_quit{$chatnet}{$address}; + print DATABASE "quit_all $time $chatnet $address $nick @$reason\n"; + } + } + foreach my $chatnet (keys %spoke) { + foreach my $address (keys %{$spoke{$chatnet}}) { + my $time = $spoke{$chatnet}{$address}; + print DATABASE "spoke $time $chatnet $address\n"; + } + } + foreach my $chatnet (keys %nicks) { + foreach my $address (keys %{$nicks{$chatnet}}) { + foreach my $nick (@{$nicks{$chatnet}{$address}}) { + foreach my $channel (@{$channels{$chatnet}{lc_irc $nick}}) { + print DATABASE "join $chatnet $address $nick $channel\n"; + } + } + } + } + foreach my $chatnet (keys %asked) { + foreach my $nick (keys %{$asked{$chatnet}}) { + foreach my $nick_asked (keys %{$asked{$chatnet}{$nick}}) { + my $time = $asked{$chatnet}{$nick}{$nick_asked}; + print DATABASE "ask $time $chatnet $nick $nick_asked\n"; + } + } + } + close DATABASE; + rename $database, $database_old; + rename $database_tmp, $database; +} + +######## Update the database to reflect currently joined users ######## + +sub initialize_database() { + my $time = time(); + foreach my $chatnet (keys %nicks) { + my @addresses = keys %{$nicks{$chatnet}}; + foreach my $address (@addresses) { + my @nicks = @{$nicks{$chatnet}{$address}}; + foreach my $nick (@nicks) { + do_quit $time, $chatnet, $address, $nick; + } + do_quit_all $time, $chatnet, $address, $nicks[0], ['disappeared']; + } + } + foreach my $server (Irssi::servers()) { + foreach my $channel ($server->channels()) { + foreach my $nick ($channel->nicks()) { + do_join lc $server->{chatnet}, + canonical $nick->{host}, $nick->{nick}, $channel->{name} + if $nick->{host} ne ""; + } + } + } +} + +######## Expire old entries ######## + +sub expire_database() { + my $days = Irssi::settings_get_int("seen_expire_after"); + my $time = time() - $days*24*60*60; + my %reachable_addresses = (); + foreach my $chatnet (keys %addresses) { + foreach my $address (values %{$addresses{$chatnet}}) { + $reachable_addresses{$chatnet}{$address} = 1; + } + } + foreach my $chatnet (keys %address_absent) { + foreach my $address (keys %{$address_absent{$chatnet}}) { + if ($address_absent{$chatnet}{$address} <= $time || + !$reachable_addresses{$chatnet}{$address}) { + delete $address_absent{$chatnet}{$address}; + delete $last_nicks{$chatnet}{$address}; + delete $how_quit{$chatnet}{$address}; + } + } + } + foreach my $chatnet (keys %spoke) { + foreach my $address (keys %{$spoke{$chatnet}}) { + if ($spoke{$chatnet}{$address} <= $time || + !$reachable_addresses{$chatnet}{$address}) { + delete $spoke{$chatnet}{$address}; + } + } + } + foreach my $chatnet (keys %nick_absent) { + foreach my $nick (keys %{$nick_absent{$chatnet}}) { + if ($nick_absent{$chatnet}{$nick} <= $time) { + delete $nick_absent{$chatnet}{$nick}; + delete $addresses{$chatnet}{$nick}; + delete $orig_nick{$chatnet}{$nick}; + } + } + } + my $days_asked = Irssi::settings_get_int("seen_expire_asked_after"); + my $time_asked = time() - $days_asked*24*60*60; + foreach my $chatnet (keys %asked) { + foreach my $nick (keys %{$asked{$chatnet}}) { + foreach my $nick_asks (keys %{$asked{$chatnet}{$nick}}) { + if ($asked{$chatnet}{$nick}{$nick_asks} <= $time_asked) { + delete $asked{$chatnet}{$nick}{$nick_asks}; + } + } + } + } +} + +######## Compose a description when did we see that person ######## + +sub show_reason($) { + my ($reason) = @_; + return ":" if $reason eq ""; + $reason =~ s/\cc\d\d?(,\d\d?)?|[\000-\037]//g; + return ": $reason"; +} + +sub only_public(@$) { + my $can_show = pop @_; + my @channels = (); + foreach my $channel (@_) { + if ($channel =~ /^-(.*)$/) { + push @channels, $1 if $can_show->($1); + } else { + push @channels, $channel; + } + } + return wantarray ? @channels : $channels[0]; +} + +sub is_here(\@$) { + my ($channels, $where_asks) = @_; + return if !defined $where_asks; + my $lc_where_asks = lc_irc $where_asks; + foreach my $i (0..$#{$channels}) { + if (lc_irc $channels->[$i] eq $lc_where_asks) { + splice @{$channels}, $i, 1; + return 1; + } + } + return 0; +} + +sub on_channels(@) { + return @_ == 1 ? "on the channel $_[0]" : "on the channels " . show_list(@_); +} + +our %show_how_quit = ( + disappeared => sub { + return "they disappeared. No more information is available."; + }, + was_left => sub { + my ($true_channel, $where_asks, $can_show) = @_; + my $channel = only_public $true_channel, $can_show; + return + defined $channel ? + lc_irc $channel eq lc_irc $where_asks ? + "was here and then I left" : + "was on channel $channel, when I left." : + "was on the channel when I left."; + }, + left => sub { + my ($true_channel, $reason, $where_asks, $can_show) = @_; + my $channel = only_public $true_channel, $can_show; + return + (defined $channel ? + lc_irc $channel eq lc_irc $where_asks ? + "person left" : "they left the channel $channel" : + "left because") . + show_reason($reason); + }, + quit => sub { + my ($true_channels, $reason, $where_asks, $can_show) = @_; + my @channels = only_public split(/,/, $true_channels), $can_show; + my $is_here = is_here @channels, $where_asks; + return + (@channels == 0 ? + $is_here ? "they left " : "" : + ($is_here ? "they are here " : "they were seen quitting ") . + on_channels(@channels) . + " ") . + "with the message" . show_reason($reason); + }, + was_kicked => sub { + my ($true_channel, $kicker, $reason, $where_asks, $can_show) = @_; + my $channel = only_public $true_channel, $can_show; + return + "they " . + (defined $channel ? + lc_irc $channel eq lc_irc $where_asks ? + "were kicked" : "were kicked from $channel" : + "kicked") . + " by $kicker" . show_reason($reason); + }, +); + +sub show_how_quit($$$) { + my ($how_quit, $where_asks, $can_show) = @_; + return $show_how_quit{$how_quit->[0]} + (@{$how_quit}[1..$#{$how_quit}], $where_asks, $can_show); +} + +sub show_where_is($$$$$$$) { + my ($server, $nick, $address, $where_asks, $can_show, $asked_and, $spoke_and) = @_; + my $chatnet = lc $server->{chatnet}; + my $lc_nick = lc_irc $nick; + my @nicks = @{$nicks{$chatnet}{$address}}; + @nicks = sort @nicks; + my @channels = all_channels($chatnet, @nicks); + @channels = + only_public + map ({mark_private($server->channel_find($_), $_)} sort @channels), + $can_show; + my $is_here = is_here @channels, $where_asks; + my $this_nick_absent = $nick_absent{$chatnet}{$lc_nick}; + return + (defined $this_nick_absent ? + "Queried user $nick " . + show_time_since($this_nick_absent) . + ", $asked_and${spoke_and}is now " . + show_list(@nicks) . + " " : + "Queried user $asked_and${spoke_and}$nick is currently " . + (@nicks == 1 ? "" : "(rowniez jako " . + show_list(grep {lc_irc $_ ne $lc_nick} @nicks) . ") ")) . + (@channels == 0 ? + $is_here ? "in this channel" : "on IRC" : + ($is_here ? "here on " : "") . on_channels(@channels)) . + "."; +} + +sub seen($$$$$$) { + my ($server, $nick, $who_asks, $where_asks, $can_show, $asked) = @_; + my $chatnet = lc $server->{chatnet}; + my $lc_nick = lc_irc $nick; + my $address = $addresses{$chatnet}{$lc_nick}; + unless (defined $address) { + if (defined $asked) {return "You asked- $asked about $nick.", 0, 0} + return "Sorry, I don't know of $nick.", 0, 0; + } + $nick = $orig_nick{$chatnet}{$lc_nick}; + if ($address eq canonical $server->{userhost}) { + return "I am $nick!", 1, 0; + } + if (defined $who_asks && $address eq $who_asks) { + return "You are $nick!", 1, 0; + } + my $asked_and = defined $asked ? "$asked; " : ""; + my $spoke = $spoke{$chatnet}{$address}; + my $spoke_and = defined $spoke ? + "last spoke " . show_time_since($spoke) . ". " : ""; + if (defined $address_absent{$chatnet}{$address}) { + my $last_nick = $last_nicks{$chatnet}{$address}; + my $when_address = show_time_since $address_absent{$chatnet}{$address}; + if (lc_irc $last_nick eq $lc_nick) { + return "The person with the nick $nick $asked_and$spoke_and$when_address " . + show_how_quit($how_quit{$chatnet}{$address}, + $where_asks, $can_show), 1, 1; + } else { + my $when_nick = show_time_since $nick_absent{$chatnet}{$lc_nick}; + return "Person, who $when_nick used nick $nick, " . + "$asked_and$spoke_and$when_address jako $last_nick " . + show_how_quit($how_quit{$chatnet}{$address}, + $where_asks, $can_show), 1, 1; + } + } else { + return show_where_is($server, $nick, $address, + $where_asks, $can_show, + $asked_and, $spoke_and), 1, 0; + } +} + +######## Initialization ######## + +read_database; +expire_database; +initialize_database; +write_database; + +Irssi::timeout_add 60*60*1000, sub {expire_database; write_database}, undef; + +######## Irssi signal handlers ######## + +sub can_show_this_channel($) { + my ($channel) = @_; + my $lc_channel = lc_irc $channel; + return sub {lc_irc $_[0] eq $lc_channel}; +} + +sub can_show_his_channels($$) { + my ($chatnet, $nick) = @_; + my $lc_nick = lc_irc $nick; + my @channels = $channels{$chatnet}{$lc_nick} ? + @{$channels{$chatnet}{$lc_nick}} : (); + return sub { + my $channel = lc_irc $_[0]; + return grep {lc_irc $_ eq $channel} @channels; + }; +} + +sub check_asked($$$) { + my ($chatnet, $server, $nick) = @_; + my $lc_nick = lc_irc $nick; + my $who_asked = $asked{$chatnet}{$lc_nick}; + return unless $who_asked; + foreach my $nick_asked (sort {$who_asked->{$a} <=> $who_asked->{$b}} + keys %{$who_asked}) { + my $when_asked = show_time_since $who_asked->{$nick_asked}; + my ($reply, $found, $remember_asked) = + seen $server, $nick_asked, undef, undef, + can_show_his_channels($chatnet, $nick), + "was looking for you $when_asked"; + $server->command("notice $nick $reply"); + do_forget_ask $chatnet, $nick, $nick_asked; + append_to_database "forget_ask $chatnet $nick $nick_asked"; + } +} + +Irssi::signal_add "channel wholist", sub { + my ($channel) = @_; + my $server = $channel->{server}; + my $chatnet = lc $server->{chatnet}; + foreach my $nick ($channel->nicks()) { + my $lc_nick = lc_irc $nick->{nick}; + my $lc_channel = lc_irc $channel->{name}; + on_join $chatnet, canonical $nick->{host}, $nick->{nick}, $channel->{name} + unless $nick->{host} eq "" || + $channels{$chatnet}{$lc_nick} && + grep {lc_irc $_ eq $lc_channel} @{$channels{$chatnet}{$lc_nick}}; + check_asked $chatnet, $server, $nick->{nick}; + } +}; + +Irssi::signal_add_first "channel destroyed", sub { + my ($channel) = @_; + my $chatnet = lc $channel->{server}{chatnet}; + foreach my $nick ($channel->nicks()) { + on_part $chatnet, canonical $nick->{host}, $nick->{nick}, $channel->{name}, + ['was_left', mark_private($channel, $channel->{name})] + unless $nick->{host} eq ""; + } +}; + +Irssi::signal_add "event join", sub { + my ($server, $args, $nick, $address) = @_; + $args =~ /^:(.*)$/ or $args =~ /^([^ ]+)$/ or return; + my $channel = $1; + my $chatnet = lc $server->{chatnet}; + on_join $chatnet, canonical $address, $nick, $channel; + check_asked $chatnet, $server, $nick; +}; + +Irssi::signal_add "event part", sub { + my ($server, $args, $nick, $address) = @_; + $args =~ /^([^ ]+) +:(.*)$/ or $args =~ /^([^ ]+) +([^ ]+)$/ or $args =~ /^([^ ]+)()$/ or return; + my ($channel, $reason) = ($1, $2); + my $chatnet = lc $server->{chatnet}; + return if defined $nick_absent{$chatnet}{lc_irc $nick}; + $reason = "" if $reason eq $nick; + on_part $chatnet, canonical $address, $nick, $channel, + ['left', mark_private($server->channel_find($channel), $channel), $reason]; +}; + +Irssi::signal_add "event quit", sub { + my ($server, $args, $nick, $address) = @_; + $args =~ /^:(.*)$/ or $args =~ /^([^ ]+)$/ or $args =~ /^()$/ or return; + my $reason = $1; + my $chatnet = lc $server->{chatnet}; + my $lc_nick = lc_irc $nick; + return if defined $nick_absent{$chatnet}{$lc_nick}; + $reason = "" if $reason =~ /^(Quit: )?(leaving)?$/; + my @channels = $channels{$chatnet}{$lc_nick} ? + @{$channels{$chatnet}{$lc_nick}} : (); + on_quit $chatnet, canonical $address, $nick, + ['quit', join(",", map {mark_private($server->channel_find($_), $_)} sort @channels), $reason]; +}; + +Irssi::signal_add "event kick", sub { + my ($server, $args, $kicker, $kicker_address) = @_; + $args =~ /^([^ ]+) +([^ ]+) +:(.*)$/ or $args =~ /^([^ ]+) +([^ ]+) +([^ ]+)$/ or + $args =~ /^([^ ]+) +([^ ]+)()$/ or return; + my ($channel, $nick, $reason) = ($1, $2, $3); + my $chatnet = lc $server->{chatnet}; + $reason = "" if $reason eq $kicker; + on_part $chatnet, $addresses{$chatnet}{lc_irc $nick}, $nick, $channel, + ['was_kicked', mark_private($server->channel_find($channel), $channel), $kicker, $reason]; +}; + +Irssi::signal_add "event nick", sub { + my ($server, $args, $old_nick, $address) = @_; + $args =~ /^:(.*)$/ or $args =~ /^([^ ]+)$/ or return; + my $new_nick = $1; + return if $address eq ""; + my $chatnet = lc $server->{chatnet}; + on_nick $chatnet, canonical $address, $old_nick, $new_nick; + check_asked $chatnet, $server, $new_nick; +}; + +######## Commands ######## + +Irssi::command_bind "seen", sub { + my ($args, $server, $target) = @_; + my $nick; + if ($args =~ /^ *([^ ]+) *$/) { + $nick = $1; + } else { + Irssi::print "Usage: /seen <nick>"; + return; + } + unless ($server && $server->{connected}) { + Irssi::print "Not connected to server"; + return; + } + my ($reply, $found, $remember_asked) = + seen $server, $nick, undef, undef, sub {1}, undef; + Irssi::print $reply; +}; + +Irssi::command_bind "say_seen", sub { + my ($args, $server, $target) = @_; + my $chatnet = lc $server->{chatnet}; + my ($nick_asks, $prefix, $nick); + if ($args =~ /^ *([^ ]+) *$/) { + $nick_asks = undef; + $prefix = ""; + $nick = $1; + } elsif ($args =~ /^ *([^ ]+) +([^ ]+) *$/) { + $nick_asks = $1; + $prefix = "$1: "; + $nick = $2; + } else { + Irssi::print "Usage: /say_seen [<to_whom>] <nick>"; + return; + } + unless ($server && $server->{connected}) { + Irssi::print "Not connected to server"; + return; + } + unless ($target) { + Irssi::print "Not in a channel or query"; + return; + } + my $can_show = + $target->{type} eq 'CHANNEL' ? + can_show_this_channel($target->{name}) : + $target->{type} eq 'QUERY' ? + can_show_his_channels($chatnet, $target->{name}) : + sub {0}; + my ($reply, $found, $remember_asked) = + seen $server, $nick, undef, $target->{name}, $can_show, undef; + on_ask $chatnet, $nick, $nick_asks + if defined $nick_asks && $remember_asked; + $server->command("msg $target->{name} $prefix$reply"); +}; + +sub cmd_listen_switch($$$$) { + my ($state, $args, $server, $target) = @_; + if ($args =~ /^ *$/) { + unless ($server && $server->{connected}) { + Irssi::print "Not connected to server"; + return; + } + unless ($target && $target->{type} eq 'CHANNEL') { + Irssi::print "Not in a channel"; + return; + } + on_listen lc $server->{chatnet}, lc_irc $target->{name}, $state; + } elsif ($args =~ /^ *([^ ]+) *$/) + { + unless ($server && $server->{connected}) { + Irssi::print "Not connected to server"; + return; + } + on_listen lc $server->{chatnet}, lc_irc $1, $state; + } elsif ($args =~ /^ *([^ ]+) +([^ ]+) *$/) + { + on_listen lc $1, lc_irc $2, $state; + } else { + Irssi::print "Usage: /listen $state [[<chatnet>] <channel>]"; + } +} + +Irssi::command_bind "listen", sub { + my ($args, $server, $target) = @_; + Irssi::command_runsub "listen", $args, $server, $target; +}; + +Irssi::command_bind "listen on", sub { + my ($args, $server, $target) = @_; + cmd_listen_switch "on", $args, $server, $target; +}; + +Irssi::command_bind "listen off", sub { + my ($args, $server, $target) = @_; + cmd_listen_switch "off", $args, $server, $target; +}; + +Irssi::command_bind "listen delay", sub { + my ($args, $server, $target) = @_; + cmd_listen_switch "delay", $args, $server, $target; +}; + +Irssi::command_bind "listen private", sub { + my ($args, $server, $target) = @_; + cmd_listen_switch "private", $args, $server, $target; +}; + +Irssi::command_bind "listen disable", sub { + my ($args, $server, $target) = @_; + cmd_listen_switch "disable", $args, $server, $target; +}; + +our @joined_text = (" ", "joined"); + +Irssi::command_bind "listen list", sub { + my ($args, $server, $target) = @_; + if ($args =~ /^ *$/) { + my %all_channels = (); + foreach my $server (Irssi::servers()) { + my $chatnet = lc $server->{chatnet}; + foreach my $channel ($server->channels()) { + $all_channels{$chatnet}{lc_irc $channel->{name}}[0] = 1; + } + } + foreach my $chatnet (keys %listen_on) { + foreach my $channel (keys %{$listen_on{$chatnet}}) { + $all_channels{$chatnet}{$channel}[1] = $listen_on{$chatnet}{$channel}; + } + } + my $max_chatnet_width = 1; + my $max_channel_width = 1; + foreach my $chatnet (keys %all_channels) { + $max_chatnet_width = length $chatnet + if length $chatnet > $max_chatnet_width; + foreach my $channel (keys %{$all_channels{$chatnet}}) { + $max_channel_width = length $channel + if length $channel > $max_channel_width; + } + } + Irssi::print "'seen' is listening:"; + foreach my $chatnet (sort keys %all_channels) { + foreach my $channel (sort keys %{$all_channels{$chatnet}}) { + Irssi::print + $chatnet . + " " x ($max_chatnet_width - length ($chatnet) + 1) . + $channel . + " " x ($max_channel_width - length ($channel) + 3) . + $joined_text[$all_channels{$chatnet}{$channel}[0]] . + " " . + $all_channels{$chatnet}{$channel}[1]; + } + } + } else { + Irssi::print "Usage: /listen list"; + } +}; + +Irssi::command_bind "forget", sub { + my ($args, $server, $target) = @_; + my $nick; + if ($args =~ /^ *([^ ]+) *$/) { + $nick = $1; + } else { + Irssi::print "Usage: /forget <nick>"; + return; + } + unless ($server) { + Irssi::print "Not connected to server"; + return; + } + my $chatnet = lc $server->{chatnet}; + return unless $asked{$chatnet}{$nick}; + foreach my $nick_asked (keys %{$asked{$chatnet}{$nick}}) { + do_forget_ask $chatnet, $nick, $nick_asked; + append_to_database "forget_ask $chatnet $nick $nick_asked"; + } +}; + +######## Listen to seen requests from other people ######## + +our $last_reply = undef; +our $last_asked = undef; + +our %pending_replies = (); + +sub seen_reply($$$$$$) { + my ($server, $nick_asks, $address, $target, $nick, $sure) = @_; + my $chatnet = lc $server->{chatnet}; + my ($reply, $found, $remember_asked) = + seen $server, $nick, $address, $target, + can_show_this_channel($target), undef; + return unless $sure || $found; + unless ($reply eq $last_reply && $nick eq $last_asked) { + Irssi::print "[$target] $nick_asks: $reply"; + $server->command("msg $target $nick_asks: $reply"); + $last_reply = $reply; + $last_asked = $nick; + } + on_ask $chatnet, $nick, $nick_asks if $remember_asked; +} + +sub private_seen_reply($$$$$$) { + my ($server, $nick_asks, $address, $target, $nick, $sure) = @_; + my $chatnet = lc $server->{chatnet}; + my ($reply, $found, $remember_asked) = + seen $server, $nick, $address, undef, + can_show_his_channels($chatnet, $nick_asks), undef; + return unless $sure || $found; + $server->command("notice $nick_asks $reply"); + $server->command("notice $nick_asks " . + "You can also ask about the presence of people privately, e.g. /msg $server->{nick} seen $nick"); + on_ask $chatnet, $nick, $nick_asks if $remember_asked; +} + +sub delayed_seen_reply($$$$$$) { + my ($server, $nick_asks, $address, $target, $nick, $sure) = @_; + my $chatnet = lc $server->{chatnet}; + my $lc_nick = lc_irc $nick; + return if defined $pending_replies{$chatnet}{$target}{$lc_nick}; + my $timeout = Irssi::settings_get_int("seen_delay") * 1000; + $pending_replies{$chatnet}{$target}{$lc_nick} = Irssi::timeout_add_once $timeout, sub { + delete $pending_replies{$chatnet}{$target}{$lc_nick}; + seen_reply $server, $nick_asks, $address, $target, $nick, $sure; + }, undef; +} + +our %reply_method = ( + on => \&seen_reply, + off => undef, + delay => \&delayed_seen_reply, + private => \&private_seen_reply, + disable => undef, +); + +sub check_another_seen($$$$) { + my ($chatnet, $channel, $msg, $nick_asks) = @_; + my $lc_channel = lc_irc $channel; + if ($listen_on{$chatnet}{$lc_channel} eq 'delay') { + foreach my $nick (keys %{$pending_replies{$chatnet}{$channel}}) { + my $nick_regexp = lc_irc_regexp $nick; + if ($msg =~ /(^|[ \cb])$nick_regexp($|[ !,.:;?\cb])/ || + lc_irc $nick_asks eq $nick) { + my $tag = $pending_replies{$chatnet}{$channel}{$nick}; + Irssi::timeout_remove $tag; + delete $pending_replies{$chatnet}{$channel}{$nick}; + } + } + } +} + +Irssi::signal_add "message public", sub { + my ($server, $msg, $nick_asks, $address, $channel) = @_; + my $chatnet = lc $server->{chatnet}; + $address = canonical $address; + on_spoke $chatnet, $address; + my $lc_channel = lc_irc $channel; + my ($msg_body, $func) = + $msg =~ /^\Q$server->{nick}\E(?:|:|\cb:\cb) +(.*)$/i ? ($1, \&seen_reply) : + ($msg, $reply_method{$listen_on{$chatnet}{$lc_channel} || 'off'}); + if (defined $func) { + my $sure = + $msg_body =~ $seen_regexp ? 1 : + $msg_body =~ $maybe_seen_regexp1 || + $msg_body =~ $maybe_seen_regexp2 ? 0 : + undef; + if (defined $sure) { + my $nick = $1; + return if $sure == 0 && $nick =~ $exclude_regexp; + Irssi::signal_continue @_; + $func->($server, $nick_asks, $address, $channel, $nick, $sure); + return; + } + } + check_another_seen $chatnet, $channel, $msg, $nick_asks; +}; + +Irssi::signal_add "message irc notice", sub { + my ($server, $msg, $nick_asks, $address, $target) = @_; + my $chatnet = lc $server->{chatnet}; + check_another_seen $chatnet, $target, $msg, $nick_asks; +}; + +Irssi::signal_add "message private", sub { + my ($server, $msg, $nick_asks, $address) = @_; + my $chatnet = lc $server->{chatnet}; + on_spoke $chatnet, canonical $address; + check_asked $chatnet, $server, $nick_asks; + my $sure = + $msg =~ $seen_regexp ? 1 : + $msg =~ $maybe_seen_regexp1 || + $msg =~ $maybe_seen_regexp2 ? 0 : + undef; + if (defined $sure) { + my $nick = $1; + my ($reply, $found, $remember_asked) = + seen $server, $nick, canonical $address, undef, + can_show_his_channels($chatnet, $nick_asks), undef; + return unless $sure || $found; + Irssi::signal_continue @_; + $server->command("msg $nick_asks $reply"); + on_ask $chatnet, $nick, $nick_asks if $remember_asked; + } +}; + +Irssi::signal_add "message irc action", sub { + my ($server, $msg, $nick, $address, $target) = @_; + on_spoke lc $server->{chatnet}, canonical $address; +}; diff --git a/spaceslash.pl b/spaceslash.pl new file mode 100644 index 0000000..73ea73a --- /dev/null +++ b/spaceslash.pl @@ -0,0 +1,35 @@ +#!/usr/bin/perl + +use warnings; +use strict; + +use Irssi qw/signal_continue signal_stop active_server command signal_add/; + +our $VERSION = "0.1"; +our %IRSSI = ( + authors => 'Urchlay', + contact => 'Urchlay on NewNet', + name => 'spaceslash', + description => 'treat " /command" as though the space weren\'t there', + license => 'Same as Perl', + url => 'none', +); + +sub sig_send_text { + if($_[0] =~ s{^\s+/}{/}) { + if($_[0] =~ s{/\s+/\s*}{/}) { + signal_continue(@_); + return; + } + + signal_stop(); + my $s = active_server(); + if($s) { + $s->command($_[0]); + } else { + command($_[0]); + } + } +} + +signal_add("send text", \&sig_send_text); diff --git a/trap_stdin.pl b/trap_stdin.pl new file mode 100644 index 0000000..61e5c8e --- /dev/null +++ b/trap_stdin.pl @@ -0,0 +1,114 @@ +#!/usr/bin/perl -w + +package TrapStdin; + +sub TIEHANDLE { + my $class = shift; + my $fh; + bless \$fh, $class +} + +sub READLINE { + my (undef, $file, $line) = caller; + warn "Irssi scripts can't read from STDIN or ARGV at $file line $line.\n"; + return undef; +} + +sub READ { + goto &READLINE; +} + +package main; + +tie *TRAP, 'TrapStdin'; +*ARGV = *STDIN = *TRAP; + +#### rest of this file is documentation. the doc is a lot longer than +#### the code, sorry about that. + +=pod + +=encoding utf8 + +=head1 NAME + +trap_stdin.pl - catch attempts to read from standard input in an irssi perl script. + +=head1 SYNOPSIS + +# from shell: + + cp trap_stdin.pl ~/.irssi/scripts/autorun/ + +# if irssi was already running, load manually: + + /script load trap_stdin + +=head1 DESCRIPTION + +This script is intended for script developers. + +The Perl scripting interface to irssi doesn't support reading from +B<STDIN> (or from B<ARGV> via the B<E<lt>E<gt>> operator). Attempts +to do so cause irssi to freeze: it stops responding to either +the keyboard or incoming IRC traffic. Eventually, the IRC server +will disconnect the client, since it's no longer responding to +PING messages, but the user will have to kill the client manually +(e.g. using C<kill> from another terminal), and probably also have to +blindly type C<reset> to fix the terminal afterwards. + +The problem is, the script is trying to read from the terminal, but +the terminal has already been "taken over" by irssi itself. The read +blocks, waiting for input it will never receive. + +It's easy to avoid this: just don't try to read from standard input in +your scripts. Problem solved? Well, it would be, but... + +The author of this script has an unfortunate tendency to forget the +filehandle when using B<E<lt>E<gt>>. Example: + + open my $fh, "<", $filename; + my $content = <>; + +The second line was I<intended> to be: + + my $content = <$fh>; + +...but due to PEBKAC, the C<$fh> was left out (passive voice sucks: +I<I> screwed up and left it out). Attempting to run the broken code +causes irssi to lock up, with no indication what the problem is. +Although the code is wrong, the penalty for this trivial mistake is +harsh. + +With B<trap_stdin.pl> loaded, the erroneous code will instead cause a +warning, and irssi will continue executing normally. The warning looks +like: + + Irssi scripts can't read from STDIN or ARGV at *FILE* line *LINE*. + +=head1 NOTES + +All loaded scripts are affected by this. It shouldn't have any effect +on scripts that already work correctly, since they already don't try +to read from standard input (or else they wouldn't work correctly). + +B<trap_stdin.pl> doesn't actually B<use Irssi>. It also doesn't stay +isolated to its own package namespace: it works by reassigning the +global C<*STDIN> and C<*ARGV> globs to a tied filehandle. + +Unlike a normal irssi script, the changes made by this one will stay +active even if the script is unloaded. + +If we didn't care about getting an irssi-specific warning, the script +could have just been: + + undef *ARGV; + undef *STDIN; + +=head1 AUTHOR + +B. Watson (urchlay@slackware.uk), aka Urchlay on Libera. + +=head1 LICENSE + +Licensed under the WTFPL. See http://www.wtfpl.net/txt/copying/ for details. diff --git a/unifmt.pl b/unifmt.pl new file mode 100644 index 0000000..7622c0f --- /dev/null +++ b/unifmt.pl @@ -0,0 +1,611 @@ +#!/usr/bin/perl + +# TODO: +# Braille. + +# to read the docs outside of irssi: perldoc /path/to/unifmt.pl +# in irssi, "/script load unifmt.pl", then "/unifmt_help" + +=encoding utf8 + +=pod + +=head1 NAME + +unifmt.pl - unicode text formatting for irssi + +=head1 SYNOPSIS + +in shell: cp unifmt.pl ~/.irssi/scripts/ + +in irssi: /script load unifmt.pl + +=head1 DESCRIPTION + +unifmt.pl adds keystrokes to irssi that allow you to type double-width +ASCII (Unicode FF00 block) characters, blackletter characters, and +use Unicode combining characters to make your text appear underlined +(single or double line), struck out, or "slashed" out. + +Rather than executing the script as an irssi slash-command, the modes are +controlled via keystrokes. This allows the formatted text to be mixed +with normal or differently-formatted text on the same line of input. + +There are 2 classes of formatting: "transforms", which convert plain +Latin alphabetics to other characters (such as fraktur or italic), and +"combines", which use plain Latin alphabetics followed by a combining +character (such as an underline). + +You can mix combines and transforms, e.g. underlined fraktur, but only +one combine and one transform can be enabled at a time (so you can't +do e.g. both underline and strikethrough, or both subscript and bold +serif). + +=head1 KEYSTROKES + +All the formatting controls must be preceded by a prefix character, +which defaults B<^F> (control-F). + +=over 4 + +=head2 Transforms + +=item B<^F w> + +Enables wide formatting. Each character you type is replaced by its +double-width equivalent from the Unicode B<U+FF01-U+FFE1> range, if +it has one. Only characters B<U+0021-U+007E> (AKA the printable ASCII +charset) have double-width equivalents. As a special case, the space +(B<U+0020>) character is replaced with B<U+3000>, B<IDEOGRAPHIC SPACE>, which +is a double-width space. All other characters are treated normally. + + Example: This is wide text + +=item B<^F b> + +Bold sans serif. + + Example: 𝗧𝗵𝗶𝘀 𝗶𝘀 𝗯𝗼𝗹𝗱 𝘀𝗮𝗻𝘀 + +=item B<^F i> + +Italic sans serif + + Example: 𝘛𝘩𝘪𝘴 𝘪𝘴 𝘪𝘵𝘢𝘭𝘪𝘤 𝘴𝘢𝘯𝘴 + +=item B<^F j> + +Bold italic sans serif + + Example: 𝙏𝙝𝙞𝙨 𝙞𝙨 𝙗𝙤𝙡𝙙 𝙞𝙩𝙖𝙡𝙞𝙘 𝙨𝙖𝙣𝙨 + +=item B<^F B> + +Bold serif. + + Example: 𝐓𝐡𝐢𝐬 𝐢𝐬 𝐛𝐨𝐥𝐝 𝐬𝐞𝐫𝐢𝐟 + +=item B<^F I> + +Italic serif + + Example: 𝑇ℎ𝑖𝑠 𝑖𝑠 𝑖𝑡𝑎𝑙𝑖𝑐 𝑠𝑒𝑟𝑖𝑓 + +=item B<^F J> + +Bold italic serif + + Example: 𝑻𝒉𝒊𝒔 𝒊𝒔 𝒃𝒐𝒍𝒅 𝒊𝒕𝒂𝒍𝒊𝒄 𝒔𝒆𝒓𝒊𝒇 + +=item B<^F 2> + +Double-struck + + Example: 𝕋𝕙𝕚𝕤 𝕚𝕤 𝕕𝕠𝕦𝕓𝕝𝕖-𝕤𝕥𝕣𝕦𝕔𝕜 + +=item B<^F c> + +Cursive + + Example: 𝒯𝒽𝒾𝓈 𝒾𝓈 𝒸𝓊𝓇𝓈𝒾𝓋ℯ + +=item B<^F C> + +Bold cursive + + Example: 𝓣𝓱𝓲𝓼 𝓲𝓼 𝓫𝓸𝓵𝓭 𝓬𝓾𝓻𝓼𝓲𝓿𝓮 + +=item B<^F k> + +Fraktur (aka blackletter) + + Example: 𝔗𝔥𝔦𝔰 𝔦𝔰 𝔣𝔯𝔞𝔨𝔱𝔲𝔯 + +I am aware that the Mathematical Fraktur symbols were never intended +for use as text, but this script is for fun, not for standards compliance. + +=item B<^F K> + + Example: 𝔗𝔥𝔦𝔰 𝔦𝔰 𝔣𝔯𝔞𝔨𝔱𝔲𝔯 + +Bold fraktur (aka blackletter) + + Example: 𝕿𝖍𝖎𝖘 𝖎𝖘 𝖇𝖔𝖑𝖉 𝖋𝖗𝖆𝖐𝖙𝖚𝖗 + +=item B<^F ^> + +Superscript. This isn't perfect: a few superscripted letters only +exist in Unicode as either uppercase or lowercase, so e.g. all +lowercase B<i> characters will display as superscripted capital B<I>. + + Example: ᵀʰᶦˢ ᶦˢ ˢᵘᵖᵉʳˢᶜʳᶦᵖᵗ + +=head2 Combines + +=item B<^F _> + +Enables underlining. Each character you type is followed by the Unicode +combining character B<U+0332>, B<COMBINING LOW LINE>. Example: U̲n̲d̲e̲r̲l̲i̲n̲e̲d̲ + +=item B<^F => + +Enables double underlining. Each character you type is followed by the Unicode +combining character B<U+0333>, B<COMBINING DOUBLE LOW LINE>. Example: U̳n̳d̳e̳r̳l̳i̳n̳e̳d̳ + +=item B<^F -> + +Enables strikethrough. Each character you type is followed by the Unicode +combining character B<U+0336>, B<COMBINING LONG STROKE OVERLAY>. Example: S̶t̶r̶i̶k̶e̶t̶h̶r̶o̶u̶g̶h̶ + +=item B<^F /> + +Enables slashthrough. Each character you type is followed by the Unicode +combining character B<U+0338>, B<COMBINING LONG SOLIDUS OVERLAY>. Example: S̸l̸a̸s̸h̸o̸u̸t̸ + +=item B<^F ^F> + +Acts like a single B<^F> was pressed. Does not disable formatting. If +you have a regular irssi keybinding for B<^F>, it will be acted on. +Otherwise, a B<^F> will be inserted into the input buffer. + +=item B<^F F> + +Disables all the formatting modes. Actually, B<^F> followed by any character +not listed above will do the same thing, but I promise not to change +the B<^F F> combo in any future versions of this script. + +=back + +=head1 SETTINGS + +=over 4 + +=item B<unifmt_keys> + +String, the 6 remappable keystrokes used to enable the formatting +modes. This defaults to B<fw_=-/>. The order is: Prefix, Wide, +Underline, Double-Underline, Strikethrough, Slashthrough (note: only +the combines are currently remappable; transforms are not). The prefix +key is used as a control key, but when you set it, use a regular +alphabetic (e.g. don't say B<^X>, just say B<x> or B<X>). + +=item B<unifmt_spaces> + +Boolean, whether or not to apply combines to spaces (default: false). + +=back + +=head1 NOTES + +For any of this to work, you'll have to enable UTF-8 in irssi, and use +a UTF-8 capable terminal. This applies to everyone else too: if you're +sending UTF-8 encoded Unicode to them, their client (and terminal if +it's a terminal client) will have to know how to display it. If not, +they'll see garbage in place of what you intended. + +This script was developed with urxvt (AKA rxvt-unicode). It should work +with any terminal that fully supports UTF-8 and Unicode... but you want +a terminal that supports looking up glyphs from a list of fonts, like +urxvt does. Otherwise, you might have a hard time finding a single font +that has all the glyphs you'll need. + +The editing keys (arrows, home/end, pgup/pgdn, and any alt-? combos) +might act strangely while the formatting modes are enabled, depending +on your terminal and TERM environment variable. Backspace and ^U +(kill line) should still work OK. This will probably be fixed in the +future. Tab-completing nicks doesn't work either, but fixing that will +be a huge PITA (or maybe impossible). + +None of the formatting modes persist past the end of the current line of +input. Pressing Enter always clears all the modes. This is to minimize +annoyance, as there's no visual indicator of which mode(s) you're in. + +Before you use this script on a public channel, you'd better make sure +the channel doesn't have rules against using fancy Unicode. You may +annoy the other users, and/or find yourself banned. + +tmux doesn't seem to be capable of actually displaying the wide + +combining character combinations. They render as plain wide. If you +copy/paste them to another window (a terminal not running tmux for +instance), they show up correctly. So tmux "knows" the formatting is +there, but doesn't display it. + +The underline, strike, slashout combinations don't work with screen, +and probably never will. I'd love to be proven wrong, so let me know if +you get it working there. I at least can see the wide characters with +"screen -U". + +=head1 AUTHOR + +Urchlay <urchlay@slackware.uk> + +=head1 LICENSE + +WTFPL: Do WTF you want with this. + +=head1 SEE ALSO + +irssi(1), urxvt(1), unicode(7), utf-8(7) + +=cut + +use utf8; +use feature 'unicode_strings'; + +our $VERSION = "0.2"; +our %IRSSI = ( + authors => 'Urchlay', + contact => 'Urchlay on FreeNode', + name => 'unifmt', + description => 'Fancy Unicode text formatting', + license => 'WTFPL', + url => 'https://slackware.uk/~urchlay/repos/misc-scripts', +); + +use warnings; +use strict; + +# 20200827 bkw: adding gui_input_get_pos to the list of imports causes +# this script to fail to autoload when irssi starts (but it'll load OK +# if manually loaded after startup). I only use it for debugging anyway. +use Irssi qw{ + command command_bind parse_special signal_register + signal_add_first signal_add_last signal_continue signal_emit + signal_stop settings_set_str settings_get_str settings_add_str + settings_get_bool settings_add_bool + }; + +our $SELF = $IRSSI{name}; +our $default_keys = "fw_=-/"; + +##sub xf_wide { +## my $key = shift; +## if($key == 0x20) { +## $key = 0x3000; +## } elsif($key >= 0x21 && $key <= 0x7e) { +## $key += 0xfee0; +## } +## return $key; +##} + +our %transforms = ( + 'Wide' => \&xf_wide, + 'Bold Serif' => \&xf_boldserif, + 'Italic Serif' => \&xf_italserif, + 'Bold Italic Serif' => \&xf_bolditalserif, + 'Bold Sans' => \&xf_boldsans, + 'Italic Sans' => \&xf_italsans, + 'Bold Italic Sans' => \&xf_bolditalsans, + 'Double-struck' => \&xf_doublestrike, + 'Cursive' => \&xf_cursive, + 'Bold Cursive' => \&xf_boldcursive, + 'Fraktur' => \&xf_fraktur, + 'Bold Fraktur' => \&xf_boldfraktur, + 'Superscript' => \&xf_superscript, +); + +our %transform_keys = ( + w => 'Wide', + B => 'Bold Serif', + I => 'Italic Serif', + J => 'Bold Italic Serif', + b => 'Bold Sans', + i => 'Italic Sans', + j => 'Bold Italic Sans', + 2 => 'Double-struck', + c => 'Cursive', + C => 'Bold Cursive', + k => 'Fraktur', + K => 'Bold Fraktur', + '^' => 'Superscript', +); + + +# Holds a reference to one of the xf_* subs, or undef if no +# transform is active. +our $transform; + +# These 2 are controlled by setting unifmt_keys: +our $prefix_key; +our %combining_map; + +# Which of combining_map is active, or 0 for none +our $combining_char = 0; + +# True if the last keypress was ^F +our $was_prefix = 0; + +# 2 if the last keypress was escape, 1 if the last 2 were escape and [, +# 0 otherwise. +our $was_escape = 0; + + +# There's no way to enable debugging without editing the script. +our $DEBUG = 0; + +# Only used for debugging. +our $count = 0; + +# Ditto. +sub dump_buf { + my $buf = parse_special('$L', 0, 0); + my $len = length($buf); + my $pos = Irssi::gui_input_get_pos(); + my $out = "pos==" . $pos . " "; + for(my $i = 0; $i < $len; $i++) { + my $star = ($i == $len ? "*" : ""); + $out .= sprintf("$star%02x ", ord(substr($buf, $i, 1))); + } + print $out; +} + +#### +# transforms take numeric Unicode codepoint arg, and return +# a numeric Unicode codepoint. + +sub xf_wide { + my $key = shift; + + if($key == 0x20) { + $key = 0x3000; # wide space, maybe better to avoid this? + } elsif($key >= 0x21 && $key <= 0x7e) { + # unicode 0x0f01 to 0x0fee are wide versions of ASCII + $key += 0xfee0; + } + # else pass it through as-is + + return $key; +} + +sub xf_alpha_map { + my $k = shift; + my $map = shift; + + ## warn "k was $k"; + if($k >= 65 && $k <= 90) { # A-Z + $k = ord(substr($map, $k - 65)); + } elsif($k >= 97 && $k <= 122) { # a-z + $k = ord(substr($map, $k - 97 + 26)); + } + ## warn "k now $k"; + + return $k; +} + +sub xf_boldserif { + return xf_alpha_map($_[0], "𝐀𝐁𝐂𝐃𝐄𝐅𝐆𝐇𝐈𝐉𝐊𝐋𝐌𝐍𝐎𝐏𝐐𝐑𝐒𝐓𝐔𝐕𝐖𝐗𝐘𝐙𝐚𝐛𝐜𝐝𝐞𝐟𝐠𝐡𝐢𝐣𝐤𝐥𝐦𝐧𝐨𝐩𝐪𝐫𝐬𝐭𝐮𝐯𝐰𝐱𝐲𝐳"); +} + +sub xf_italserif { + return xf_alpha_map($_[0], "𝐴𝐵𝐶𝐷𝐸𝐹𝐺𝐻𝐼𝐽𝐾𝐿𝑀𝑁𝑂𝑃𝑄𝑅𝑆𝑇𝑈𝑉𝑊𝑋𝑌𝑍𝑎𝑏𝑐𝑑𝑒𝑓𝑔ℎ𝑖𝑗𝑘𝑙𝑚𝑛𝑜𝑝𝑞𝑟𝑠𝑡𝑢𝑣𝑤𝑥𝑦𝑧"); +} + +sub xf_bolditalserif { + return xf_alpha_map($_[0], "𝑨𝑩𝑪𝑫𝑬𝑭𝑮𝑯𝑰𝑱𝑲𝑳𝑴𝑵𝑶𝑷𝑸𝑹𝑺𝑻𝑼𝑽𝑾𝑿𝒀𝒁𝒂𝒃𝒄𝒅𝒆𝒇𝒈𝒉𝒊𝒋𝒌𝒍𝒎𝒏𝒐𝒑𝒒𝒓𝒔𝒕𝒖𝒗𝒘𝒙𝒚𝒛"); +} + +sub xf_boldsans { + return xf_alpha_map($_[0], "𝗔𝗕𝗖𝗗𝗘𝗙𝗚𝗛𝗜𝗝𝗞𝗟𝗠𝗡𝗢𝗣𝗤𝗥𝗦𝗧𝗨𝗩𝗪𝗫𝗬𝗭𝗮𝗯𝗰𝗱𝗲𝗳𝗴𝗵𝗶𝗷𝗸𝗹𝗺𝗻𝗼𝗽𝗾𝗿𝘀𝘁𝘂𝘃𝘄𝘅𝘆𝘇"); +} + +sub xf_italsans { + return xf_alpha_map($_[0], "𝘈𝘉𝘊𝘋𝘌𝘍𝘎𝘏𝘐𝘑𝘒𝘓𝘔𝘕𝘖𝘗𝘘𝘙𝘚𝘛𝘜𝘝𝘞𝘟𝘠𝘡𝘢𝘣𝘤𝘥𝘦𝘧𝘨𝘩𝘪𝘫𝘬𝘭𝘮𝘯𝘰𝘱𝘲𝘳𝘴𝘵𝘶𝘷𝘸𝘹𝘺𝘻"); +} + +sub xf_bolditalsans { + return xf_alpha_map($_[0], "𝘼𝘽𝘾𝘿𝙀𝙁𝙂𝙃𝙄𝙅𝙆𝙇𝙈𝙉𝙊𝙋𝙌𝙍𝙎𝙏𝙐𝙑𝙒𝙓𝙔𝙕𝙖𝙗𝙘𝙙𝙚𝙛𝙜𝙝𝙞𝙟𝙠𝙡𝙢𝙣𝙤𝙥𝙦𝙧𝙨𝙩𝙪𝙫𝙬𝙭𝙮𝙯"); +} + +sub xf_doublestrike { + return xf_alpha_map($_[0], "𝔸𝔹ℂ𝔻𝔼𝔽𝔾ℍ𝕀𝕁𝕂𝕃𝕄ℕ𝕆ℙℚℝ𝕊𝕋𝕌𝕍𝕎𝕏𝕐ℤ𝕒𝕓𝕔𝕕𝕖𝕗𝕘𝕙𝕚𝕛𝕜𝕝𝕞𝕟𝕠𝕡𝕢𝕣𝕤𝕥𝕦𝕧𝕨𝕩𝕪𝕫"); +} + +sub xf_cursive { + return xf_alpha_map($_[0], "𝒜ℬ𝒞𝒟ℰℱ𝒢ℋℐ𝒥𝒦ℒℳ𝒩𝒪𝒫𝒬ℛ𝒮𝒯𝒰𝒱𝒲𝒳𝒴𝒵𝒶𝒷𝒸𝒹ℯ𝒻ℊ𝒽𝒾𝒿𝓀𝓁𝓂𝓃ℴ𝓅𝓆𝓇𝓈𝓉𝓊𝓋𝓌𝓍𝓎𝓏"); +} + +sub xf_boldcursive { + return xf_alpha_map($_[0], "𝓐𝓑𝓒𝓓𝓔𝓕𝓖𝓗𝓘𝓙𝓚𝓛𝓜𝓝𝓞𝓟𝓠𝓡𝓢𝓣𝓤𝓥𝓦𝓧𝓨𝓩𝓪𝓫𝓬𝓭𝓮𝓯𝓰𝓱𝓲𝓳𝓴𝓵𝓶𝓷𝓸𝓹𝓺𝓻𝓼𝓽𝓾𝓿𝔀𝔁𝔂𝔃"); +} + +sub xf_fraktur { + return xf_alpha_map($_[0], "𝔄𝔅ℭ𝔇𝔈𝔉𝔊ℌℑ𝔍𝔎𝔏𝔐𝔑𝔒𝔓𝔔ℜ𝔖𝔗𝔘𝔙𝔚𝔛𝔜ℨ𝔞𝔟𝔠𝔡𝔢𝔣𝔤𝔥𝔦𝔧𝔨𝔩𝔪𝔫𝔬𝔭𝔮𝔯𝔰𝔱𝔲𝔳𝔴𝔵𝔶𝔷"); +} + +sub xf_boldfraktur { + return xf_alpha_map($_[0], "𝕬𝕭𝕮𝕯𝕰𝕱𝕲𝕳𝕴𝕵𝕶𝕷𝕸𝕹𝕺𝕻𝕼𝕽𝕾𝕿𝖀𝖁𝖂𝖃𝖄𝖅𝖆𝖇𝖈𝖉𝖊𝖋𝖌𝖍𝖎𝖏𝖐𝖑𝖒𝖓𝖔𝖕𝖖𝖗𝖘𝖙𝖚𝖛𝖜𝖝𝖞𝖟"); +} + +sub xf_superscript { + return xf_alpha_map($_[0], "ᴬᴮᶜᴰᴱᶠᴳᴴᴵᴶᴷᴸᴹᴺᴼᴾᑫᴿˢᵀᵁⱽᵂˣʸᶻᵃᵇᶜᵈᵉᶠᵍʰᶦʲᵏˡᵐⁿᵒᵖᑫʳˢᵗᵘᵛʷˣʸᶻ"); +} +#### + + +sub get_ctrl_key { + return ord(uc($_[0])) - 0x40; +} + +sub fmt_key { + # \002 is "toggle bold" + return "'\002" . $_[0] . "\002'"; +} + +sub init_keys { + our $default_keys; + + my $keys = settings_get_str('unifmt_keys'); + if(length $keys != 6) { + print "$SELF: Invalid unifmt_keys, should be 6 keystrokes, defaulting to '$default_keys'"; + settings_set_str('unifmt_keys', ($keys = $default_keys) ); + } + + my ($p, $w, $u, $d, $s, $l) = split "", $keys; + + $p = get_ctrl_key($p); + if($p < 0 || $p > 0x1f) { + my $pd = uc substr($default_keys, 0, 1); + print "$SELF: Invalid prefix key, defaulting to " . fmt_key("^" . $pd); + $p = get_ctrl_key($pd); + } + + our $prefix_key = $p; + + our %combining_map = ( + $u => 0x332, # underline + $d => 0x333, # double underline + $s => 0x336, # strikethrough + $l => 0x338, # slash-through + ); + + print "$SELF: " . + "prefix " . fmt_key("^" . chr($prefix_key + 0x40)) . ", " . + "underline " . fmt_key($u) . ", " . + "double " . fmt_key($d) . ", " . + "strike " . fmt_key($s) . ", " . + "slash " . fmt_key($l); + + for(sort { $transform_keys{$a} cmp $transform_keys{$b} } keys %transform_keys) { + print $transform_keys{$_} . ": " . $_; + } +} + +sub handle_keypress { + my $key = shift; + + if($DEBUG) { printf $count++ . ": got key 0x%x", $key; dump_buf(); } + + # hackish way to let most escape codes through unmodified. assumes + # (incorrectly) that all escape codes are either Esc-[-(something), + # 3 bytes... or else Esc-(something that isn't [), 2 bytes. This + # happens to let urxvt's arrow keys and alt-numbers through, at least. + if($was_escape) { + if($was_escape == 2 && $key != ord('[')) { + $was_escape = 0; + } else { + $was_escape--; + } + signal_continue($key); + return; + } + + # don't try to combine with combining chars! + for(values our %combining_map) { + # warn "$key $_"; + if($key == $_) { + signal_continue($key); + return; + } + } + + # ctrl-space is mapped to the null character. make it dump the + # current input buffer contents in hex, if debugging is active. + if($DEBUG && $key == 0) { + dump_buf(); + signal_stop(); + return; + } + + # ^F pressed once: set flag, but don't insert into buffer. + # Pressed twice = unset flag, inset into buffer. + if($key == $prefix_key) { + if($was_prefix) { + $was_prefix = 0; + signal_continue($key); + } else { + $was_prefix = 1; + signal_stop(); + } + return; + } + + # enter/return, turn off formatting + if($key == 0x0d || $key == 0x0a) { + $combining_char = $was_prefix = 0; + undef $transform; + signal_continue($key); + return; + } + + # backspace/delete and control characters are acted on normally, except + # that escape has to set a flag + if($key == 0x7f || $key < 0x20) { + if($key == 0x1b) { + $was_escape = 2; + } + signal_continue($key); + return; + } + + # last key pressed was ^F, act on it, but don't insert into the buffer + if($was_prefix) { + # warn "prefix key pressed before " . $key; + my $t = $transform_keys{chr($key)}; + if(defined($t)) { + # warn "transform key $key"; + $transform = $transforms{$t}; + } else { + $combining_char = $combining_map{chr($key)} || 0; + # unrecognized keys also turn off transform modes + undef $transform unless $combining_char; + } + + $was_prefix = 0; + signal_stop(); + return; + } + + if(defined $transform) { + $key = $transform->($key); + } + + signal_continue($key); + + # if it was a space and we're not formatting spaces, we're done + if(($key == 0x20 || $key == 0x3000) && !settings_get_bool('unifmt_spaces')) { + return; + } + + if($combining_char) { + if($DEBUG) { print "combining($key, $combining_char)"; } + signal_emit('gui key pressed', $combining_char); + } +} + +sub unifmt_help { + command("/exec - pod2text " . __FILE__); +} + +### main() +settings_add_str($SELF, 'unifmt_keys', $default_keys); +settings_add_bool($SELF, 'unifmt_spaces', 0); +init_keys(); + +signal_add_last('setup changed', \&init_keys); + +signal_register({ "gui key pressed", [ "integer" ] }); +signal_add_first("gui key pressed", \&handle_keypress); + +command_bind("unifmt_help", \&unifmt_help); + +print "$SELF.pl loaded, /unifmt_help for help" diff --git a/upsidedown.pl b/upsidedown.pl new file mode 100644 index 0000000..07960d8 --- /dev/null +++ b/upsidedown.pl @@ -0,0 +1,242 @@ +#!/usr/bin/perl + +use warnings; +use strict; + +use Irssi qw/command command_bind/; + +our $VERSION = "0.1"; +our %IRSSI = ( + authors => 'Urchlay', + contact => 'Urchlay on FreeNode', + name => 'upsidedown', + description => '', + license => 'WTFPL', + url => 'none', +); + +my %flip_table = ( + "\N{U+0021}" => "\N{U+00A1}", + "\N{U+0022}" => "\N{U+201E}", + "\N{U+0026}" => "\N{U+214B}", + "\N{U+0027}" => "\N{U+002C}", + "\N{U+0028}" => "\N{U+0029}", + "\N{U+002E}" => "\N{U+02D9}", + "\N{U+0033}" => "\N{U+0190}", + "\N{U+0034}" => "\N{U+152D}", + "\N{U+0036}" => "\N{U+0039}", + "\N{U+0037}" => "\N{U+2C62}", + "\N{U+003B}" => "\N{U+061B}", + "\N{U+003C}" => "\N{U+003E}", + "\N{U+003F}" => "\N{U+00BF}", + "\N{U+0041}" => "\N{U+2200}", + "\N{U+0042}" => "\N{U+10412}", + "\N{U+0043}" => "\N{U+2183}", + "\N{U+0044}" => "\N{U+25D6}", + "\N{U+0045}" => "\N{U+018E}", + "\N{U+0046}" => "\N{U+2132}", + "\N{U+0047}" => "\N{U+2141}", + "\N{U+004A}" => "\N{U+017F}", + "\N{U+004B}" => "\N{U+22CA}", + "\N{U+004C}" => "\N{U+2142}", + "\N{U+004D}" => "\N{U+0057}", + "\N{U+004E}" => "\N{U+1D0E}", + "\N{U+0050}" => "\N{U+0500}", + "\N{U+0051}" => "\N{U+038C}", + "\N{U+0052}" => "\N{U+1D1A}", + "\N{U+0054}" => "\N{U+22A5}", + "\N{U+0055}" => "\N{U+2229}", + "\N{U+0056}" => "\N{U+1D27}", + "\N{U+0059}" => "\N{U+2144}", + "\N{U+005B}" => "\N{U+005D}", + "\N{U+005F}" => "\N{U+203E}", + "\N{U+0061}" => "\N{U+0250}", + "\N{U+0062}" => "\N{U+0071}", + "\N{U+0063}" => "\N{U+0254}", + "\N{U+0064}" => "\N{U+0070}", + "\N{U+0065}" => "\N{U+01DD}", + "\N{U+0066}" => "\N{U+025F}", + "\N{U+0067}" => "\N{U+0183}", + "\N{U+0068}" => "\N{U+0265}", + "\N{U+0069}" => "\N{U+0131}", + "\N{U+006A}" => "\N{U+027E}", + "\N{U+006B}" => "\N{U+029E}", + "\N{U+006C}" => "\N{U+0283}", + "\N{U+006D}" => "\N{U+026F}", + "\N{U+006E}" => "\N{U+0075}", + "\N{U+0072}" => "\N{U+0279}", + "\N{U+0074}" => "\N{U+0287}", + "\N{U+0076}" => "\N{U+028C}", + "\N{U+0077}" => "\N{U+028D}", + "\N{U+0079}" => "\N{U+028E}", + "\N{U+007B}" => "\N{U+007D}", + "\N{U+203F}" => "\N{U+2040}", + "\N{U+2045}" => "\N{U+2046}", + "\N{U+2234}" => "\N{U+2235}", + "\N{U+00A1}" => "\N{U+0021}", + "\N{U+201E}" => "\N{U+0022}", + "\N{U+214B}" => "\N{U+0026}", + "\N{U+002C}" => "\N{U+0027}", + "\N{U+0029}" => "\N{U+0028}", + "\N{U+02D9}" => "\N{U+002E}", + "\N{U+0190}" => "\N{U+0033}", + "\N{U+152D}" => "\N{U+0034}", + "\N{U+0039}" => "\N{U+0036}", + "\N{U+2C62}" => "\N{U+0037}", + "\N{U+061B}" => "\N{U+003B}", + "\N{U+003E}" => "\N{U+003C}", + "\N{U+00BF}" => "\N{U+003F}", + "\N{U+2200}" => "\N{U+0041}", + "\N{U+10412}" => "\N{U+0042}", + "\N{U+2183}" => "\N{U+0043}", + "\N{U+25D6}" => "\N{U+0044}", + "\N{U+018E}" => "\N{U+0045}", + "\N{U+2132}" => "\N{U+0046}", + "\N{U+2141}" => "\N{U+0047}", + "\N{U+017F}" => "\N{U+004A}", + "\N{U+22CA}" => "\N{U+004B}", + "\N{U+2142}" => "\N{U+004C}", + "\N{U+0057}" => "\N{U+004D}", + "\N{U+1D0E}" => "\N{U+004E}", + "\N{U+0500}" => "\N{U+0050}", + "\N{U+038C}" => "\N{U+0051}", + "\N{U+1D1A}" => "\N{U+0052}", + "\N{U+22A5}" => "\N{U+0054}", + "\N{U+2229}" => "\N{U+0055}", + "\N{U+1D27}" => "\N{U+0056}", + "\N{U+2144}" => "\N{U+0059}", + "\N{U+005D}" => "\N{U+005B}", + "\N{U+203E}" => "\N{U+005F}", + "\N{U+0250}" => "\N{U+0061}", + "\N{U+0071}" => "\N{U+0062}", + "\N{U+0254}" => "\N{U+0063}", + "\N{U+0070}" => "\N{U+0064}", + "\N{U+01DD}" => "\N{U+0065}", + "\N{U+025F}" => "\N{U+0066}", + "\N{U+0183}" => "\N{U+0067}", + "\N{U+0265}" => "\N{U+0068}", + "\N{U+0131}" => "\N{U+0069}", + "\N{U+027E}" => "\N{U+006A}", + "\N{U+029E}" => "\N{U+006B}", + "\N{U+0283}" => "\N{U+006C}", + "\N{U+026F}" => "\N{U+006D}", + "\N{U+0075}" => "\N{U+006E}", + "\N{U+0279}" => "\N{U+0072}", + "\N{U+0287}" => "\N{U+0074}", + "\N{U+028C}" => "\N{U+0076}", + "\N{U+028D}" => "\N{U+0077}", + "\N{U+028E}" => "\N{U+0079}", + "\N{U+007D}" => "\N{U+007B}", + "\N{U+2040}" => "\N{U+203F}", + "\N{U+2046}" => "\N{U+2045}", + "\N{U+2235}" => "\N{U+2234}", +); + +# originally from javascript code at +# https://twiki.org/cgi-bin/view/Blog/BlogEntry201211x1 +# turned into perl and expanded upon somewhat. +our %mirror_table = ( + 'A' => 'A', + 'B' => "\N{U+A4ED}", #"\N{U+1660}", + 'C' => "\N{U+0186}", + 'D' => "\N{U+15E1}", + 'E' => "\N{U+018E}", + 'F' => "\N{U+A7FB}", #"\N{U+15B7}", + 'G' => "\N{U+2202}", # "\N{U+13AE}", # need better G + 'H' => 'H', + 'I' => 'I', + 'J' => "\N{U+10B1}", # same as lowercase :( + 'K' => "\N{U+A4D8}", # "\N{U+1434}", + 'L' => "\N{U+2143}", + 'M' => 'M', + 'N' => "\N{U+0418}", + 'O' => 'O', + 'P' => "\N{U+A7FC}", # may be \u146B + 'Q' => "O\N{U+0327}", # "\N{U+1ECC}", + 'R' => "\N{U+042F}", + 'S' => "\N{U+01A7}", + 'T' => 'T', + 'U' => 'U', + 'V' => 'V', + 'W' => 'W', + 'X' => 'X', + 'Y' => 'Y', + 'Z' => "\\\N{U+0305}\N{U+0332}", #"\N{U+01B8}", + 'a' => "\N{U+0252}", + 'b' => 'd', + 'c' => "\N{U+0254}", + 'd' => 'b', + 'e' => "\N{U+0258}", + 'f' => "\N{U+0287}", + 'g' => "\N{U+01EB}", + 'h' => "\N{U+029C}", + 'i' => 'i', + 'j' => "\N{U+10B1}", # same as uppercase + 'k' => "\N{U+029E}", + 'l' => "\N{U+222B}", #"\N{U+2321}", # not sure about this one + 'm' => 'm', + 'n' => 'n', + 'o' => 'o', + 'p' => 'q', + 'q' => 'p', + 'r' => "\N{U+027F}", # maybe U+1D19, ᴙ + 's' => "\N{U+01A8}", + 't' => "\N{U+019A}", + 'u' => 'u', + 'v' => 'v', + 'w' => 'w', + 'x' => 'x', + 'y' => 'y', # need better y + 'z' => 's', # need better z + '0' => '0', + '1' => '1', # need better 1 + '2' => '2', # need better 2 + '3' => "\N{U+01B8}", # or maybe 0664 or 0510 + '4' => "\N{U+0662}", + '5' => '5', # need better 5 + '6' => "\N{U+2202}", + '7' => "\N{U+0393}", + '?' => "\N{U+241A}", + ';' => "\N{U+204F}", + '(' => ')', + ')' => '(', + '[' => ']', + ']' => '[', + '<' => '>', + '>' => '<', + '{' => '}', + '}' => '{', + '\\' => '/', + '/' => '\\', + ',' => "\N{U+2E41}", + '~' => "\N{U+223D}", +); + +sub mirror { + my ($text, $srv, $chan) = @_; + return unless $text; + return unless $srv; + return unless $chan; + + $text = reverse $text; + $text =~ s/(.)/$mirror_table{$1}||$1/ge; + $chan->command('MSG ' . $chan->{name} . ' ' . $text); +} + +sub flip_rev { + my ($text, $srv, $chan) = @_; + flip(scalar(reverse $text), $srv, $chan); +} + +sub flip { + my ($text, $srv, $chan) = @_; + return unless $text; + return unless $srv; + return unless $chan; + $text =~ s/(.)/$flip_table{$1}||$1/ge; + $chan->command('MSG ' . $chan->{name} . ' ' . reverse($text)); +} + +command_bind("flip", \&flip_rev); +command_bind("usd", \&flip); +command_bind("mirror", \&mirror); diff --git a/urlcleaner.pl b/urlcleaner.pl new file mode 100644 index 0000000..a1dc0f6 --- /dev/null +++ b/urlcleaner.pl @@ -0,0 +1,103 @@ +#!/usr/bin/perl + +# Avoid spamming your friends with *huge* URLs from ebay and amazon. + +# The extra junk at the end of the URL is session and tracking +# info. It's probably not a security problem to let people see it, but +# it does hurt their eyes. + +# What this script does is: + +# 1. Modify any message from you (after you hit Enter and before it +# goes to the IRC server) and remove all the CGI params from URLs +# matching https://www.ebay.com or https://www.amazon.com. + +# 2. Modify any amazon/ebay URLs in incoming public or private message, +# the same way, before they're printed in your irssi window. You +# will see only the trimmed version. + +# If you *really* want to paste the full URLs, you still can: just +# prefix your line with -- (the -- goes at the front of your input, +# not directly in front of the URL). + +# If you *really* want to see the original URL, use the /untrim +# command. It'll show the previous (up to) 3 URLs in their original +# forms. The list is cleared after printing. + +# Sample URLs: +# https://www.amazon.com/Fender-Original-Instrument-Straight-Straight-18-6-Foot/dp/B07YSRML8M?pd_rd_w=APcow&content-id=amzn1.sym.55c0153f-1fb7-42ff-8241-d1c0f3732289&pf_rd_p=55c0153f-1fb7-42ff-8241-d1c0f3732289&pf_rd_r=QM0V2R6T6MXQAH39WATH&pd_rd_wg=Fsarl&pd_rd_r=58846f08-1923-4e41-9191-9cf3dc7bf8ae&ref_=sspa_dk_detail_img_1&sp_csd=d2lkZ2V0TmFtZT1zcF9kZXRhaWxfdGhlbWF0aWM&th=1 +# https://www.ebay.com/itm/225034857497?_skw=2%22+round+iron-on+patch&itmmeta=01JDKKYWPHG9RB0ENME87YF4WX&hash=item34651f6c19:g:JdYAAOSwTPhirSFD&itmprp=enc%3AAQAJAAAA8HoV3kP08IDx%2BKZ9MfhVJKkDq1GsENxQhDILz02EizByT9tnvQUmV4MujHHWVdHvzDDR1hGWeP2UUa7RpgCE6e1iSZDTiRBaKCuEHmRQKKOHf3tkFUKhVfo0M2r86ABaDgxmu9zvRrZsee0ZR2J41YUwcrmOBwtor3hFjzLto7UWULyUuydtBHTwrzWaegH0YIzpRqiv3fJx6mgopXNBME%2FuTIK2B%2ByyoyC07G6ZMYuCjsxXcwMUVB9qxuRjD6t3%2BiW%2B1bKLkkcB + +# For both amazon and ebay, none of the CGI params are needed. Just chop +# off everything from ? on. + +# TODO: clean up youtube URLs. Not sure what all the CGI params are, or which +# are redundant/unwanted. Domains: +# youtube.com +# m.youtube.com +# youtu.be +# youtube.googleapis.com +# youtubei.googleapis.com +# Some others are listed here: +# https://support.google.com/a/answer/6214622?hl=en +# ...but we only care about ones that support $domain/watch?v=... + +use Irssi qw/ + signal_add_first + signal_continue + signal_register + command_bind +/; + +our $VERSION = "0.1"; +our %IRSSI = ( + authors => 'Urchlay', + contact => 'Urchlay on Libera', + name => 'urlcleaner', + description => 'trim down ebay and amazon URLs', + license => 'WTFPL', + url => 'none', +); + +our @orig_urls = (); + +sub push_url { + return if grep { $_ eq $_[0] } @orig_urls; # skip dups + push @orig_urls, $_[0]; + shift @orig_urls if @orig_urls > 3; +} + +sub trim_url { + my $old = $_[0]; + $_[0] =~ s{((https://www.(?:ebay|amazon).com\S+)\?\S*)}{$2}; + push_url($1) if $old ne $_[0]; + return $_[0]; +} + +sub on_send_text { + if($_[0] !~ /^\s*--/) { + $_[0] = trim_url($_[0]); + } + signal_continue(@_); +} + +sub on_public_msg { + $_[1] = trim_url($_[1]); + signal_continue(@_); +} + +sub cmd_untrim { + print "No trimmed URLs" unless @orig_urls; + print "Original URL:\n$_" for @orig_urls; + @orig_urls = (); +} + +# I'm not 100% sure this signal_register is correct. It seems like I should be +# specifying more arguments (since on_send_text() actually gets 3 args). It +# does, however, work on at least irssi-1.4.4. + +signal_register({ "send text", [ "string" ] }); +signal_add_first("send text", "on_send_text"); +signal_add_first("message public", "on_public_msg"); +signal_add_first("message private", "on_public_msg"); +command_bind("untrim", "cmd_untrim"); @@ -0,0 +1,112 @@ +#!/usr/bin/perl + +=pod + +=head1 NAME + +wide.pl - print double-width characters + +=head1 SYNOPSIS + +=head2 From the shell: + +wide.pl <[args ...]> + +=head2 From within irssi: + +/script load wide.pl + +/wide [args] + +=head1 DESCRIPTION + +wide.pl converts printable ASCII characters to their double-width +equivalent from the Unicode 0xff block. See: + +https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block) + +Characters that don't have double-wide equivalents are simply printed +with a trailing space. + +This script can be run standalone or as an irssi /script. For things to work +properly in irssi, you'll need UTF-8 and Unicode support in irssi and in the +terminal you're using to run irssi. + +When run standalone with no arguments, reads from stdin and writes to stdout. +If arguments are given, they're treated as input (not filenames). Use shell +redirection to read from a file: wide.pl < message.txt + +=head1 AUTHOR + +B. Watson <urchlay@slackware.uk>, aka Urchlay on Libera IRC. + +=head1 LICENSE + +wide.pl is released under the WTFPL: Do WTF you want with this. + +=cut + +use warnings; +use strict; + +sub wide { + my $text = shift; + my $res = ""; + + for (split "", $text) { + my $c = ord($_); + if($c >= 0x21 && $c <= 0x7e) { + $res .= chr($c + 0xfee0); + } else { + $res .= "$_$_"; + } + } + + return $res; +} + +# main() if we're not running under irssi +if(__PACKAGE__ eq 'main') { + no warnings 'utf8'; + + exec "perldoc $0" if @ARGV && $ARGV[0] =~ /--?h(elp)?/; + + if(@ARGV) { + print wide($_) for @ARGV; + print "\n"; + } else { + while(<>) { + chomp; + print wide($_); + print "\n"; + } + } + + exit 0; +} + +# irssi stuff here +require Irssi; +Irssi->import(qw/command command_bind/); + +our $VERSION = "0.1"; +our %IRSSI = ( + authors => 'Urchlay', + contact => 'Urchlay on FreeNode', + name => 'wide', + description => 'print double-width characters', + license => 'WTFPL', + url => 'none', + ); + +sub cmd_wide { + my ($text, $srv, $chan) = @_; + + return unless $text; + return unless $srv; + return unless $chan; + + $chan->command('MSG ' . $chan->{name} . ' ' . wide($text)); +} + +command_bind("wide", \&cmd_wide); diff --git a/yttitle.pl b/yttitle.pl new file mode 100644 index 0000000..a1aaf3a --- /dev/null +++ b/yttitle.pl @@ -0,0 +1,194 @@ +#!/usr/bin/perl + +no strict; + +use POSIX ":sys_wait_h"; + +use Irssi qw/ + signal_add_first + signal_continue + signal_register + command_bind + timeout_add_once + get_irssi_dir +/; + +our $VERSION = "0.1"; +our %IRSSI = ( + authors => 'Urchlay', + contact => 'Urchlay on Libera', + name => 'yttitle', + description => 'get titles for youtube videos using yt-dlp', + license => 'WTFPL', + url => 'none', +); + +# video ID => title +our %cache; + +our $tmpdir = get_irssi_dir . "/yttitle.tmp"; + +# pid => [server, target, video ID] +our %jobs; + +# video ID keys (values meaningless; only care about key presence) +our %job_videos; + +# attempts to spawn more jobs than this are just ignored. +our $maxjobs = 10; + +# milliseconds: how often to check %jobs to see if any jobs are done. +our $queue_time = 1000; + +# command to execute, 1st %s replaced with video ID, 2nd with tmp filename. +# --socket-timeout arg is seconds, should be longer that $timeout. +our $command_fmt = + "yt-dlp -q --print '%%(title)s' --socket-timeout 10 -- %s >%s 2>/dev/null"; + +our $DEBUG = 1; + +sub debug { + Irssi::print(join "", @_) if $DEBUG; +} + +sub debugf { + Irssi::print(sprintf(@_)) if $DEBUG; +} + +sub start_timer { + timeout_add_once($queue_time, "check_jobs", undef); +} + +sub get_tmp_filename { + my $id = shift; + return $tmpdir . "/" . $id; +} + +sub read_tmp_file { + my $file = shift; + + open my $fh, "<:encoding(UTF-8)", $file; + + if(!$fh) { + debug("read_tmp_file() failed to open $file: $!"); + return; + } + + my $result = <$fh>; + close $fh; + + chomp $result if defined $result; + return length($result) ? $result : undef; +} + +sub spawn_job { + my ($server, $target, $video_id) = @_; + my $jobcount = keys %jobs; + + debug("spawn_job() called, video_id $video_id"); + + if($jobcount > $maxjobs) { + debug("spawn_job(): jobcount $jobcount > maxjobs $maxjobs, ignoring request"); + return; + } + + if($job_videos{$video_id}) { + debug("spawn_job(): video_id $video_id already in queue"); + return; + } + + my $cmd = sprintf($command_fmt, $video_id, get_tmp_filename($video_id)); + debug("spawn_job() command is: $cmd"); + + my $pid = fork(); + + if($pid) { + # parent + debug("spawn_job() forked, kid pid is $pid"); + start_timer unless keys %jobs; + $jobs{$pid} = [ $server, $target, $video_id ]; + $job_videos{$video_id} = 1; + debug("spawn_job(): job pids: " . join(",", keys %jobs)); + } else { + # child, debug() and Irssi::print don't work, here. + exec $cmd; + } +} + +sub job_done { + my $pid = shift; + + my ($server, $target, $video_id) = @{$jobs{$pid}}; + + debug("job_done() pid $pid, target $target, video_id $video_id"); + + my $file = get_tmp_filename($video_id); + my $title = read_tmp_file($file); + + unlink $file; + + $cache{$video_id} = $title; + delete $job_videos{$video_id}; + + if(!defined($title)) { + debug("job_done(): video_id $video_id failed to get title"); + return; + } + + say_title($server, $target, $video_id, $title); +} + +sub check_jobs { + my @k = keys %jobs; + + debug("check_jobs(): " . @k . " jobs"); + return unless @k; + + for my $jobpid (@k) { + debug("check_jobs(): about to waitpid($jobpid, WNOHANG)"); + $pid = waitpid($jobpid, WNOHANG); + + debug("check_jobs(): jobpid $jobpid, pid $pid"); + + next if $pid == 0; # still running, let it run + job_done($jobpid) if $pid > 0; # -1 is "no such pid" + delete $jobs{$jobpid}; + } + + start_timer if keys %jobs; +} + +sub say_title { + my ($server, $target, $video_id, $title) = @_; + my $tag = "YouTube" . ($DEBUG ? "($video_id)" : ""); + $server->command("msg $target $tag: $title"); +} + +sub on_public_msg { + my ($server, $msg, $nick, $address, $target) = @_; + my $mynick = $server->{nick}; + + unless(length $target) { + $target = $nick; + $nick = $mynick; + } + + if($target eq $mynick) { + # private message... send response to sender + $target = $nick; + } + + for my $video_id ($msg =~ /(?:youtube\.com\S+(?:embed\/|v=)|youtu.be\/)([-0-9a-zA-Z_]{11})/g) { + if($cache{$video_id}) { + debug("video_id $video_id found in cache"); + say_title($server, $target, $video_id, $cache{$video_id}); + } else { + debug("video_id $video_id NOT found in cache, queuing job"); + spawn_job($server, $target, $video_id); + } + } +} + +### main() +mkdir $tmpdir; +signal_add_first("message public", "on_public_msg"); |