aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorB. Watson <urchlay@slackware.uk>2024-12-26 17:08:34 -0500
committerB. Watson <urchlay@slackware.uk>2024-12-26 17:08:34 -0500
commit9eed830f296dab257759f5276d0963467007aa6b (patch)
tree83d6225d248ec8d4026b514d8b9b248e6c0fce32
downloadstupid-irssi-tricks-9eed830f296dab257759f5276d0963467007aa6b.tar.gz
initial commit
-rw-r--r--README31
-rwxr-xr-xbitmaptext.pl2791
-rw-r--r--colors_per_channel.pl103
-rw-r--r--complete_text.pl142
-rw-r--r--frotzglue.pl445
-rw-r--r--help_path_completion.pl43
-rw-r--r--jumble.pl45
-rw-r--r--newdice.pl205
-rw-r--r--seen.pl1201
-rw-r--r--spaceslash.pl35
-rw-r--r--trap_stdin.pl114
-rw-r--r--unifmt.pl611
-rw-r--r--upsidedown.pl242
-rw-r--r--urlcleaner.pl103
-rwxr-xr-xwide.pl112
-rw-r--r--yttitle.pl194
16 files changed, 6417 insertions, 0 deletions
diff --git a/README b/README
new file mode 100644
index 0000000..ab202d0
--- /dev/null
+++ b/README
@@ -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',
+);
diff --git a/seen.pl b/seen.pl
new file mode 100644
index 0000000..85d4a2c
--- /dev/null
+++ b/seen.pl
@@ -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");
diff --git a/wide.pl b/wide.pl
new file mode 100755
index 0000000..3cf5294
--- /dev/null
+++ b/wide.pl
@@ -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");