summaryrefslogtreecommitdiffstats
path: root/scripts/chankeys.pl
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/chankeys.pl')
-rw-r--r--scripts/chankeys.pl570
1 files changed, 570 insertions, 0 deletions
diff --git a/scripts/chankeys.pl b/scripts/chankeys.pl
new file mode 100644
index 0000000..0dfb782
--- /dev/null
+++ b/scripts/chankeys.pl
@@ -0,0 +1,570 @@
+# chankeys.pl — Irssi script for associating key shortcuts with channels
+#
+# © 2021–22 martin f. krafft <madduck@madduck.net>
+# Released under the MIT licence.
+#
+### Usage:
+#
+# /script load chankeys
+#
+# This plugin serves to simplify the assignment of keyboard shortcuts that
+# take you to channels or queries (so-called "window items").
+#
+# Let's assume you're in the #irssi channel, then you could issue the command
+#
+# /chankeys add meta-s-meta-i
+#
+# and thenceforth, hitting that key combination will take you to the channel.
+# It's smart enough to check whether a mapping is already in use by chankey,
+# or whether a key combination won't work, for instance because meta-s was
+# already assigned elsewhere in the above.
+#
+# You can also explicitly specify the name (and chatnet) if you'd like to
+# set up a mapping for another item:
+#
+# /chankeys add F12 &bitlbee
+#
+# Key bindings are removed when you leave a channel or a query is closed, and
+# reinstated when the channel or query is reinstated. They are saved to
+# ~/.irssi/chankeys on /save, and loaded from there on startup and /reload.
+#
+### To-do:
+#
+# * Mappings for {01..99} and associated hook to renumber windows with named
+# mappings
+# * Handle queries better, i.e. they should be created if not found, probably
+# just use /query instead of /window goto
+# * When adding a keymap from /chankey add, if the keymap is already assigned
+# to another channel, we need to handle this better
+# * check_for_existing_bind really hurts and causes a bit of lag in Irssi that
+# it doesn't recover from for a few seconds after load. Better to read /bind
+# output once into a hash and use that.
+#
+use strict;
+use warnings;
+use Irssi;
+use version;
+
+our %IRSSI = (
+ authors => 'martin f. krafft',
+ contact => 'madduck@madduck.net',
+ name => 'chankeys',
+ description => 'manage channel keyboard shortcuts',
+ license => 'MIT',
+ version => '0.4.1',
+ changed => '2022-11-18'
+);
+
+our $VERSION = $IRSSI{version};
+my $_VERSION = version->parse($VERSION);
+
+### DEFAULTS AND SETTINGS ######################################################
+
+my $map_file = Irssi::get_irssi_dir()."/chankeys";
+my $go_command = 'window goto $C';
+my $autosave = 1;
+my $overwrite_binds = 0;
+my $clear_composites = 0;
+my $debug = 0;
+
+Irssi::settings_add_str('chankeys', 'chankeys_go_command', $go_command);
+Irssi::settings_add_bool('chankeys', 'chankeys_autosave', $autosave);
+Irssi::settings_add_bool('chankeys', 'chankeys_overwrite_binds', $overwrite_binds);
+Irssi::settings_add_bool('chankeys', 'chankeys_clear_composites', $clear_composites);
+Irssi::settings_add_bool('chankeys', 'chankeys_debug', $debug);
+
+sub sig_setup_changed {
+ $debug = Irssi::settings_get_bool('chankeys_debug');
+ $clear_composites = Irssi::settings_get_bool('chankeys_clear_composites');
+ $overwrite_binds = Irssi::settings_get_bool('chankeys_overwrite_binds');
+ $autosave = Irssi::settings_get_bool('chankeys_autosave');
+ $go_command = Irssi::settings_get_str('chankeys_go_command');
+}
+Irssi::signal_add('setup changed', \&sig_setup_changed);
+Irssi::signal_add('setup reread', \&sig_setup_changed);
+sig_setup_changed();
+
+my $changed_since_last_save = 0;
+
+my %itemmap;
+my %leadkeys;
+
+### HELPERS ####################################################################
+
+sub say {
+ my ($msg, $level, $inwin) = @_;
+ $level = $level // MSGLEVEL_CLIENTCRAP;
+ if ($inwin) {
+ Irssi::active_win->print("chankeys: $msg", $level);
+ }
+ else {
+ Irssi::print("chankeys: $msg", $level);
+ }
+}
+
+sub debug {
+ return unless $debug;
+ my ($msg, $inwin) = @_;
+ $msg = $msg // "";
+ say("DEBUG: ".$msg, MSGLEVEL_CRAP + MSGLEVEL_NO_ACT, $inwin);
+}
+
+sub info {
+ my ($msg, $inwin) = @_;
+ say($msg, MSGLEVEL_CLIENTCRAP, $inwin);
+}
+
+use Data::Dumper;
+sub dumper {
+ debug(scalar Dumper(@_), 1);
+}
+
+sub warning {
+ my ($msg, $inwin) = @_;
+ $msg = $msg // "";
+ say("WARNING: ".$msg, MSGLEVEL_CLIENTERROR, $inwin);
+}
+
+sub error {
+ my ($msg, $inwin) = @_;
+ $msg = $msg // "";
+ say("ERROR: ".$msg, MSGLEVEL_CLIENTERROR, $inwin);
+}
+
+sub channet_pair_to_string {
+ my ($name, $chatnet) = @_;
+ my $ret = $chatnet ? "$chatnet/" : '';
+ return $ret . $name;
+}
+
+sub string_to_channet_pair {
+ my ($str) = @_;
+ return reverse(split(/\//, $str));
+}
+
+sub get_keymap_for_channet_pair {
+ my ($name, $chatnet) = @_;
+ foreach my $cn ($chatnet, undef) {
+ # if not found with $chatnet, fallback to no chatnet
+ my $item = channet_pair_to_string($name, $cn);
+ my $keys = $itemmap{$item};
+ return ($keys, $name, $cn) if $keys;
+ }
+ return ();
+}
+
+sub get_go_command {
+ my ($name, $chatnet) = @_;
+ my $cmd = $go_command;
+ $cmd =~ s/\$C/$name/;
+ $cmd =~ s/\$chatnet/$chatnet/;
+ $cmd =~ s/\s+$//;
+ return $cmd;
+}
+
+my $keybind_to_check;
+my $existing_binding;
+sub check_existing_binds {
+ my ($rec, undef, $text) = @_;
+ if ($rec->{level} == 524288 and $rec->{target} eq '' and !defined $rec->{server}) {
+ if ($text =~ /^\Q${keybind_to_check}\E\s+(.+?)\s*$/) {
+ $existing_binding = $1;
+ }
+ Irssi::signal_stop();
+ }
+}
+
+sub check_for_existing_bind {
+ my ($keys) = @_;
+ $keybind_to_check = $keys;
+ $existing_binding = undef;
+ Irssi::signal_add_first('print text' => \&check_existing_binds);
+ Irssi::command("bind $keybind_to_check");
+ Irssi::signal_remove('print text' => \&check_existing_binds);
+ return $existing_binding;
+}
+
+## KEYMAP HANDLERS #############################################################
+
+sub create_keymapping {
+ my ($keys, $name, $chatnet) = @_;
+ my $cmd = 'command ' . get_go_command($name, $chatnet);
+ if ($keys =~ /(meta-.)-.+/ and !exists($leadkeys{$1})) {
+ if (my $bind = check_for_existing_bind($1)) {
+ if ($clear_composites) {
+ warning("Removing bind from $1 to '$bind' as instructed");
+ Irssi::command("^bind -delete $1");
+ $leadkeys{$1} = $bind;
+ }
+ else {
+ error("$1 is bound to '$bind' and cannot be used in composite keybinding", 1);
+ return 0;
+ }
+ }
+ }
+ Irssi::command("^bind $keys $cmd");
+ return 1;
+}
+
+sub check_create_keymapping {
+ my ($keys, $name, $chatnet) = @_;
+ my $cmd = 'command ' . get_go_command($name, $chatnet);
+ my $bind = check_for_existing_bind($keys);
+ if ($bind and $bind ne $cmd) {
+ if ($overwrite_binds) {
+ warning("Overwriting bind from $keys to '$bind' as instructed");
+ }
+ else {
+ error("Key $keys already bound to '$bind', please remove first.", 1);
+ return 0;
+ }
+ }
+ return create_keymapping($keys, $name, $chatnet);
+}
+
+sub add_keymapping {
+ my ($keys, $name, $chatnet) = @_;
+ if (check_create_keymapping($keys, $name, $chatnet)) {
+ $name = channet_pair_to_string($name, $chatnet);
+ debug("Key binding created: $keys → $name", 1);
+ return 1;
+ }
+ return 0;
+}
+
+sub remove_keymapping {
+ my ($keys) = @_;
+ my $bind = check_for_existing_bind($keys);
+ if (!$bind) {
+ error("No chankey mapping for $keys");
+ return;
+ }
+ my $item = lookup_item_by_keys($keys);
+ if ($item) {
+ Irssi::command("^bind -delete $keys");
+ return $bind;
+ }
+ else {
+ error("The key binding for '$keys' is not a chankeys binding: $bind");
+ return;
+ }
+}
+
+sub lookup_item_by_keys {
+ my ($data) = @_;
+ my $ret;
+ while (my ($item, $keys) = each %itemmap) {
+ $ret = $item if ($keys eq $data);
+ # do not call last or the iterator won't be reset
+ }
+ return $ret;
+}
+
+sub remove_existing_binds {
+ while (my ($item, $keys) = each %itemmap) {
+ Irssi::command("^bind -delete $keys");
+ }
+ %leadkeys = ();
+}
+
+### SAVING AND LOADING #########################################################
+
+sub get_mappings_fh {
+ my ($filename) = @_;
+ my $fh;
+ if (! -e $filename) {
+ save_mappings($filename);
+ info("Created new/empty mappings file: $filename");
+ }
+ open($fh, '<', $filename) || error("Cannot open mappings file: $!");
+ return $fh;
+}
+
+sub load_mappings {
+ my ($filename) = @_;
+ %itemmap = ();
+ my $fh = get_mappings_fh($filename);
+ my $firstline = <$fh> || error("Cannot read from $filename.");;
+ my $version;
+ if ($firstline =~ m/^;+\s+chankeys keymap file \(version: *([\d.]+)\)/) {
+ $version = $1;
+ }
+ else {
+ error("First line of $filename is not a chankey header.");
+ }
+
+ my $l = 1;
+ while (<$fh>) {
+ $l++;
+ next if m/^\s*(?:;|$)/;
+ my ($item, $keys, $rest) = split;
+ if ($rest) {
+ error("Cannot parse $filename:$l: $_");
+ return;
+ }
+ $itemmap{$item} = $keys;
+ }
+ close($fh) || error("Cannot close mappings file: $!");
+}
+
+sub save_mappings {
+ my ($filename) = @_;
+ open(FH, '+>', $filename) || error("Cannot create mappings file: $!");
+ print FH <<"EOF";
+; chankeys keymap file (version: $_VERSION)
+;
+; WARNING: this file will be overwritten on /save,
+; use "/set chankey_autosave off" to avoid.
+;
+; item: channel name (optionally chatnet/#channel) or query partner
+; keys: key combination
+;
+; item keys
+
+EOF
+ foreach my $name (sort keys(%itemmap)) {
+ my $keys = $itemmap{$name};
+ print FH "$name\t$keys\n";
+ }
+ print FH <<"EOF";
+
+; EXAMPLES
+;
+;;; associate meta-s-meta-i with the #irssi channel
+; libera/#irssi meta-s-meta-i
+;
+;;; associate F12 with the bitlbee control window
+; &bitlbee F12
+;
+;;; associate meta-\ with a query
+; bitlbee/sgs7e meta-\\
+
+; vim:noet:tw=0:ts=48:com=b\\:;
+EOF
+ close(FH);
+}
+
+## COMMAND HANDLERS ############################################################
+
+sub chankey_add {
+ my ($data, $server, $witem) = @_;
+ my ($keys, $name, $chatnet) = split /\s+/, $data;
+ if ($name) {
+ ($name, $chatnet) = string_to_channet_pair($name) unless $chatnet;
+ }
+ else {
+ if (!$witem) {
+ error("No active window item to add a channel key for", 1);
+ return;
+ }
+ $name = $witem->{name};
+ $chatnet = $server->{chatnet};
+ }
+ if (add_keymapping($keys, $name, $chatnet)) {
+ $itemmap{channet_pair_to_string($name, $chatnet)} = $keys;
+ $changed_since_last_save = 1;
+ }
+}
+
+sub chankey_remove {
+ my ($data) = @_;
+ return unless $data;
+ my $bind = remove_keymapping($data);
+ if ($bind) {
+ debug("Key binding removed: $data (was: $bind)");
+ my $item = lookup_item_by_keys($data);
+ delete($itemmap{$item});
+ $changed_since_last_save = 1;
+ }
+}
+
+sub chankey_list {
+ return unless %itemmap;
+ info("Key bindings I know about:", 1);
+ foreach my $item (sort keys %itemmap) {
+ my $keys = $itemmap{$item};
+ my $active;
+ if (my $bind = check_for_existing_bind($keys)) {
+ my ($name, $chatnet) = string_to_channet_pair($item);
+ $active = $bind eq ('command ' . get_go_command($name, $chatnet));
+ }
+ my $out = sprintf("%13s %1s %s", $keys, $active ? '→' : '', $item);
+ info($out, 1);
+ }
+}
+
+sub chankey_load {
+ remove_existing_binds();
+ load_mappings($map_file);
+ my $cnt = scalar(keys %itemmap);
+ foreach my $channel (Irssi::channels, Irssi::queries) {
+ my $name = $channel->{name};
+ my $chatnet = $channel->{server}->{chatnet};
+ if (my @keymap = get_keymap_for_channet_pair($name, $chatnet)) {
+ create_keymapping(@keymap);
+ }
+ }
+ $changed_since_last_save = 0;
+ info("Loaded $cnt mappings from $map_file");
+}
+
+sub chankey_save {
+ my ($args) = @_;
+ if (!$changed_since_last_save and $args ne '-force') {
+ info("Not saving unchanged mappings without -force");
+ return;
+ }
+ autosave(1);
+}
+
+sub chankey_goto {
+ my ($args) = @_;
+ my ($name, $chatnet) = split /\s+/, $args;
+ my $cmd = get_go_command($name, $chatnet);
+ Irssi::command("^$cmd");
+}
+
+Irssi::command_bind('chankeys add', \&chankey_add);
+Irssi::command_bind('chankeys remove', \&chankey_remove);
+Irssi::command_bind('chankeys list', \&chankey_list);
+Irssi::command_bind('chankeys reload', \&chankey_load);
+Irssi::command_bind('chankeys save', \&chankey_save);
+Irssi::command_bind('chankeys goto', \&chankey_goto);
+Irssi::command_bind('chankeys help', \&chankey_help);
+Irssi::command_bind('chankeys', sub {
+ my ( $data, $server, $item ) = @_;
+ $data =~ s/\s+$//g;
+ if ($data) {
+ Irssi::command_runsub('chankeys', $data, $server, $item);
+ }
+ else {
+ chankey_help();
+ }
+ }
+);
+Irssi::command_bind('help', sub {
+ $_[0] =~ s/\s+$//g;
+ return unless $_[0] eq 'chankeys';
+ chankey_help();
+ Irssi::signal_stop();
+ }
+);
+
+sub chankey_help {
+ my ($data, $server, $item) = @_;
+ Irssi::print (<<"SCRIPTHELP_EOF", MSGLEVEL_CLIENTCRAP);
+%_chankeys $_VERSION - associate key shortcuts with channels
+
+%U%_Synopsis%_%U
+
+%_CHANKEYS ADD%_ <%Ukeybinding%U> [<%Uchannel%U>] [<%Uchatnet%U>]
+%_CHANKEYS REMOVE%_ <%Ukeybinding%U>
+%_CHANKEYS LIST%_
+%_CHANKEYS [RE]LOAD%_
+%_CHANKEYS SAVE%_ [-force]
+%_CHANKEYS GOTO%_ <%Uchannel%U> [<%Uchatnet%U>]
+%_CHANKEYS HELP%_
+
+<%Ukeybinding%U> %| Key(s) to bind. Refer to %_/HELP BIND%_ for format
+<%Uchannel%U> %| Channel name to associate. Can include %_/chatnet%.
+<%Uchatnet%U> %| The chatnet of the channel. Not generally supported.
+
+%U%_Settings%_%U
+
+/set %_chankeys_go_command%_ [$go_command]
+ %| The command to use to switch to a matching window item. The only reason
+ %| you might need to set this is if you have channels with the same name
+ %| across different chatnets. In this case, you need to load the go2.pl
+ %| module, and set this to "go \$C \$chatnet", because "window goto" cannot
+ %| incorporate the chatnet (yet). Beware that this will prevent
+ %| adv_windowlist.pl from reading out the keybinding to use for the
+ %| statusbar.
+
+/set %_chankeys_overwrite_binds%_ [$overwrite_binds]
+ %| When chankey encounters an existing key mapping, it refuses to overwrite
+ %| it unless this is switched on.
+
+/set %_chankeys_clear_composites%_ [$clear_composites]
+ %| A mapping like meta-s-meta-i will not work if meta-s is bound to something
+ %| already, and chankey will check and fail in such a case. Setting this
+ %| to on will make chankeys remove the existing mapping, such that the
+ %| composite mapping works.
+
+/set %_chankeys_autosave%_ [$autosave]
+ %| Skip saving/overwriting the chankeys setup to file if you prefer to
+ %| maintain the mappings outside of irssi.
+
+/set %_chankeys_debug%_ [$debug]
+ %| Turns on debug output. Not that this may itself be buggy, so please don't
+ %| use it unless you really need it.
+
+%U%_Examples%_%U
+
+Associate %_meta-d-meta-d%_ with the current channel
+ %|%#/%_CHANKEYS ADD%_ meta-d-meta-d
+
+Associate F12 with the &bitlbee window
+ %|%#/%_BIND%_ ^[[24~ key F12
+ %|%#/%_CHANKEYS ADD%_ F12 &bitlbee
+
+Associate %_meta-m-meta-m%_ with the #matrix channel on LiberaChat
+ %|%#/%_CHANKEYS ADD%_ meta-m-meta-m #matrix LiberaChat
+
+Alternative form to specify chatnet
+ %|%#/%_CHANKEYS ADD%_ meta-m-meta-m #matrix/LiberaChat
+
+Save mappings to file ($map_file), using -force to write even if nothing has changed:
+ %|%#/%_CHANKEYS SAVE%_ -force
+
+Load mappings from file ($map_file):
+ %|%#/%_CHANKEYS LOAD%_
+
+List all known key associations
+ %|%#/%_CHANKEYS LIST%_
+SCRIPTHELP_EOF
+}
+
+## SIGNAL HANDLERS #############################################################
+
+sub on_channel_created {
+ my ($chanrec, $auto) = @_;
+ my $name = $chanrec->{name};
+ my $chatnet = $chanrec->{server}->{chatnet};
+ my @keymap = get_keymap_for_channet_pair($name, $chatnet);
+ add_keymapping(@keymap) if @keymap;
+}
+Irssi::signal_add('channel created' => \&on_channel_created);
+Irssi::signal_add('query created' => \&on_channel_created);
+
+sub on_channel_destroyed {
+ my ($chanrec) = @_;
+ my $name = $chanrec->{name};
+ my $chatnet = $chanrec->{server}->{chatnet};
+ my ($keys, undef, undef) = get_keymap_for_channet_pair($name, $chatnet);
+ remove_keymapping($keys) if $keys;
+}
+Irssi::signal_add('channel destroyed' => \&on_channel_destroyed);
+Irssi::signal_add('query destroyed' => \&on_channel_destroyed);
+
+sub autosave {
+ my ($force) = @_;
+ return unless $changed_since_last_save or $force;
+ if (!$autosave) {
+ info("Not saving mappings due to chankeys_autosave setting");
+ return;
+ }
+ info("Saving mappings to $map_file");
+ save_mappings($map_file);
+ $changed_since_last_save = 0;
+}
+
+sub UNLOAD {
+ autosave();
+}
+
+Irssi::signal_add('setup saved', \&autosave);
+Irssi::signal_add('setup reread', \&chankey_load);
+
+## INIT ########################################################################
+
+chankey_load();