From 803ad3c35fce8d62573979ccfca4386760b71dd1 Mon Sep 17 00:00:00 2001 From: Yigit Colakoglu Date: Sun, 18 Apr 2021 21:51:12 +0300 Subject: [PATCH] Changes on gentoo --- .config/X11/xinitrc | 56 +- .config/isync/mbsyncrc | 8 +- .config/mpv/scripts/mpris.so | Bin 37304 -> 0 bytes .config/ncmpcpp/config | 2 +- .config/neofetch/config.conf | 4 +- .config/qt5ct/qt5ct.conf | 2 +- .config/tmux/tmux.conf | 1 - .config/vim/plugin/plugins.vim | 18 +- .config/weechat/perl/autoload/atcomplete.pl | 90 +- .config/weechat/perl/autoload/awaylog.pl | 101 +- .config/weechat/perl/autoload/highmon.pl | 1155 +++++++++- .config/weechat/perl/autoload/iset.pl | 1646 +++++++++++++- .config/weechat/python/autoload/aesthetic.py | 71 +- .config/weechat/python/autoload/anotify.py | 477 ++++- .config/weechat/python/autoload/autosort.py | 1076 +++++++++- .../weechat/python/autoload/colorize_nicks.py | 410 +++- .config/weechat/python/autoload/go.py | 564 ++++- .config/weechat/python/autoload/styurl.py | 179 +- .config/weechat/python/autoload/vimode.py | 1885 ++++++++++++++++- .config/wgetrc | 0 .config/zsh/.zshrc | 2 +- .config/zsh/aliases | 7 +- .local/bin/backup | 3 +- .local/bin/mpd-notif | 4 +- .local/bin/status-bar/cpu-temp | 12 +- .local/bin/status-bar/volume | 2 +- .local/share/dwm/autostart.sh | 54 + .local/share/gnupg/gpg-agent.conf | 2 + .local/src/dwm/dwm.1 | 23 + .local/src/dwm/dwm.c | 84 + .../dwm-autostart-20210120-cb3f58a.diff | 179 ++ .../dwm/patches/dwm-losefullscreen-6.2.diff | 98 + .local/src/dwm/rules.h | 2 +- .local/src/dwmblocks/config.h | 2 +- .local/src/grabc/grabc | Bin 18040 -> 18112 bytes .local/src/grabc/grabc.o | Bin 14424 -> 14480 bytes .local/src/paleofetch/paleofetch.c | 16 +- .local/src/sent/sent | Bin 87440 -> 87592 bytes .local/src/st/st | Bin 111872 -> 111928 bytes .local/src/st/x.o | Bin 82632 -> 82640 bytes 40 files changed, 8121 insertions(+), 114 deletions(-) delete mode 100755 .config/mpv/scripts/mpris.so mode change 120000 => 100644 .config/weechat/perl/autoload/atcomplete.pl mode change 120000 => 100644 .config/weechat/perl/autoload/awaylog.pl mode change 120000 => 100644 .config/weechat/perl/autoload/highmon.pl mode change 120000 => 100644 .config/weechat/perl/autoload/iset.pl mode change 120000 => 100644 .config/weechat/python/autoload/aesthetic.py mode change 120000 => 100644 .config/weechat/python/autoload/anotify.py mode change 120000 => 100644 .config/weechat/python/autoload/autosort.py mode change 120000 => 100644 .config/weechat/python/autoload/colorize_nicks.py mode change 120000 => 100644 .config/weechat/python/autoload/go.py mode change 120000 => 100644 .config/weechat/python/autoload/styurl.py mode change 120000 => 100644 .config/weechat/python/autoload/vimode.py mode change 100644 => 100755 .config/wgetrc create mode 100755 .local/share/dwm/autostart.sh create mode 100644 .local/src/dwm/patches/dwm-autostart-20210120-cb3f58a.diff create mode 100644 .local/src/dwm/patches/dwm-losefullscreen-6.2.diff diff --git a/.config/X11/xinitrc b/.config/X11/xinitrc index f4a82371..d86aecaa 100755 --- a/.config/X11/xinitrc +++ b/.config/X11/xinitrc @@ -12,65 +12,14 @@ rm -rf $XDG_RUNTIME_DIR/day_cache export XSESSION_PID="$$" source ~/.config/config.env -gpgconf --dry-run --create-socketdir -#eval $(/usr/bin/gnome-keyring-daemon --start --components=pkcs11,secrets,ssh) -#export SSH_AUTH_SOCK - -clipmenud > $XDG_RUNTIME_DIR/clipmenud.out 2> $XDG_RUNTIME_DIR/clipmenud.err & -rm -f ~/.surf/tabbed-surf.xid -/bin/polkit-dumb-agent & -~/.local/bin/daily-update -~/.local/bin/keyboard > $XDG_RUNTIME_DIR/keyboard.out 2> $XDG_RUNTIME_DIR/keyboard.err -xrdb ~/.config/X11/Xresources & -~/.local/bin/mailsync & - -if [ "$NEXTCLOUD" = true ] ; then - nextcloud --background & -fi -mkdir -p ~/Downloads/neomutt -if [ "$MCONNECT" = true ] ; then - mkdir -p ~/Downloads/mconnect - (cd ~/Downloads/mconnect; mconnect -d > $XDG_RUNTIME_DIR/mconnect 2> $XDG_RUNTIME_DIR/mconnect.err &) -fi -if [ "$ACTIVITYWATCHER" = true ] ; then - pkill -f aw-watcher-window - pkill -f aw-watcher-afk - pkill -f aw-server - aw-server & - aw-watcher-window & - aw-watcher-afk & -fi -mpd -mpd-mpris & -touch ~/.cache/nextcloud-track -xss-lock -- slock & -picom --no-fading-openclose & -~/.local/bin/firefox-sync -curl 'http://yeetclock/setcolor?R=136&G=192&B=208' & - -dunst & - -xbanish -s & pactl upload-sample /usr/share/sounds/freedesktop/stereo/bell.oga x11-bell pactl load-module module-x11-bell sample=x11-bell display=$DISPLAY xset b 100 xset dpms 600 600 600 +xrdb ~/.config/X11/Xresources & -~/.local/bin/devmon --exec-on-drive "/sbin/notify-send '禍 drive mounted' '%l (%f) at %d '" \ - --exec-on-remove "/sbin/notify-send '禍 drive removed' '%l (%f) from %d '" \ - --exec-on-unmount "/sbin/notify-send '禍 drive unmounted' '%l (%f) from %d '" \ - --no-unmount --no-gui & - - -#$BROWSER & -#pass 2> /dev/null > /dev/null && qtpass & - -redshift -x 2> /dev/null > /dev/null -redshift -r -l "$LATLONG" > /dev/null 2> /dev/null & - -tmux new-session -s weechat -d weechat > /dev/null 2> /dev/null restarted=0 while true; do @@ -80,9 +29,6 @@ do notify-send -a " Desktop Manager" "Dwm Restarted" fi - dwmblocks > $XDG_RUNTIME_DIR/dwmblocks.out 2> $XDG_RUNTIME_DIR/dwmblocks.err & dwm > $XDG_RUNTIME_DIR/dwm.log 2> $XDG_RUNTIME_DIR/dwm.err sleep 0.5 done - -firefox-sync diff --git a/.config/isync/mbsyncrc b/.config/isync/mbsyncrc index c8b7c7b8..e15a4d18 100644 --- a/.config/isync/mbsyncrc +++ b/.config/isync/mbsyncrc @@ -15,8 +15,8 @@ Inbox /home/yigit/.local/share/mail/yigitcolakoglu@hotmail.com/INBOX Channel yigitcolakoglu@hotmail.com Expunge Both -Far :yigitcolakoglu@hotmail.com-remote: -Near :yigitcolakoglu@hotmail.com-local: +Master :yigitcolakoglu@hotmail.com-remote: +Slave :yigitcolakoglu@hotmail.com-local: Patterns * !"[Gmail]/All Mail" Create Both SyncState * @@ -41,8 +41,8 @@ Inbox /home/yigit/.local/share/mail/yigit@yigitcolakoglu.com/INBOX Channel yigit@yigitcolakoglu.com Expunge Both -Far :yigit@yigitcolakoglu.com-remote: -Near :yigit@yigitcolakoglu.com-local: +Master :yigit@yigitcolakoglu.com-remote: +Slave :yigit@yigitcolakoglu.com-local: Patterns * !"[Gmail]/All Mail" Create Both SyncState * diff --git a/.config/mpv/scripts/mpris.so b/.config/mpv/scripts/mpris.so deleted file mode 100755 index 251c077f2e7b1c2c3c5e54c73adb35f2b012821b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37304 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 V@%ItB?iyqIlcb{Z1oBAhzW@+~yT ['on', 'enable completion of nicks starting with @'], +); +my %options = (); + +weechat::register($SCRIPT_NAME, "David A. Golden", $VERSION, + "Apache2", "atcomplete - do nick completion following @", "", ""); +init_config(); + +weechat::hook_config("plugins.var.perl.$SCRIPT_NAME.*", "toggle_config_by_set", ""); +weechat::hook_completion("nicks", "Add @ prefix to nick completion", "complete_at_nicks", ""); + +sub complete_at_nicks { + my ($data, $completion_item, $buffer, $completion ) = @_; + return weechat::WEECHAT_RC_OK() unless $options{enabled} eq 'on'; + + my $nicklist = weechat::infolist_get("nicklist", weechat::current_buffer(), ""); + + if ($nicklist ne "") { + while (weechat::infolist_next($nicklist)) { + next unless weechat::infolist_string($nicklist, "type") eq "nick"; + my $nick = weechat::infolist_string($nicklist, "name"); + weechat::hook_completion_list_add($completion, "\@$nick", 1, weechat::WEECHAT_LIST_POS_SORT()); + } + } + + weechat::infolist_free($nicklist); + + return weechat::WEECHAT_RC_OK(); +} + +sub toggle_config_by_set { + my ($pointer, $name, $value) = @_; + $name = substr($name, length("plugins.var.perl.".$SCRIPT_NAME."."), length($name)); + $options{$name} = $value; + return weechat::WEECHAT_RC_OK(); +} + +sub init_config { + my $version = weechat::info_get("version_number", "") || 0; + foreach my $option (keys %options_default) + { + if (!weechat::config_is_set_plugin($option)) + { + weechat::config_set_plugin($option, $options_default{$option}[0]); + $options{$option} = $options_default{$option}[0]; + } + else + { + $options{$option} = weechat::config_get_plugin($option); + } + if ($version >= 0x00030500) + { + weechat::config_set_desc_plugin($option, $options_default{$option}[1]." (default: \"".$options_default{$option}[0]."\")"); + } + } +} diff --git a/.config/weechat/perl/autoload/awaylog.pl b/.config/weechat/perl/autoload/awaylog.pl deleted file mode 120000 index 200ecd52..00000000 --- a/.config/weechat/perl/autoload/awaylog.pl +++ /dev/null @@ -1 +0,0 @@ -../awaylog.pl \ No newline at end of file diff --git a/.config/weechat/perl/autoload/awaylog.pl b/.config/weechat/perl/autoload/awaylog.pl new file mode 100644 index 00000000..91e9b1b8 --- /dev/null +++ b/.config/weechat/perl/autoload/awaylog.pl @@ -0,0 +1,100 @@ +############################################################################### +# +# Copyright (c) 2008 by GolemJ +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# +############################################################################### +# +# Log highlights msg to core buffer +# You need to set "notify" to "yes" and "command" to proper command to run +# external command. You also need "shell" script to run external command. +# +# History: +# 2010-06-20, GolemJ +# version 0.8, add posibility to execute command for notification +# 2010-02-14, Emmanuel Bouthenot +# version 0.7, add colors and notifications support +# 2009-05-02, FlashCode : +# version 0.6, sync with last API changes +# 2008-11-30, GolemJ : +# version 0.5, conversion to WeeChat 0.3.0+ +# +############################################################################### + +use strict; + +weechat::register( "awaylog", "Jiri Golembiovsky", "0.8", "GPL", "Prints highlights to core buffer", "", "" ); +weechat::hook_print( "", "", "", 1, "highlight_cb", "" ); + +if( weechat::config_get_plugin( "on_away_only" ) eq "" ) { + weechat::config_set_plugin( "on_away_only", "off" ); +} + +if( weechat::config_get_plugin( "plugin_color" ) eq "" ) { + weechat::config_set_plugin( "plugin_color", "default" ); +} + +if( weechat::config_get_plugin( "name_color" ) eq "" ) { + weechat::config_set_plugin( "name_color", "default" ); +} + +if( weechat::config_get_plugin( "notify" ) eq "" ) { + weechat::config_set_plugin( "notify", "off" ); +} + +if( weechat::config_get_plugin( "command" ) eq "") { + weechat::config_set_plugin( "command", "" ); +} + +sub highlight_cb { + if( $_[5] == 1 ) { + my $away = weechat::buffer_get_string($_[1], "localvar_away"); + if (($away ne "") || (weechat::config_get_plugin( "on_away_only" ) ne "on")) + { + my $buffer_color = weechat::color(weechat::config_get_plugin( "plugin_color")) + . weechat::buffer_get_string($_[1], "plugin") + . "." + . weechat::buffer_get_string($_[1], "name") + . weechat::color("default"); + my $buffer = weechat::buffer_get_string($_[1], "plugin") + . "." + . weechat::buffer_get_string($_[1], "name"); + my $name_color = weechat::color(weechat::config_get_plugin( "name_color")) + . $_[6] + . weechat::color("default"); + my $name = $_[6]; + my $message_color = "${buffer_color} -- ${name_color} :: $_[7]"; + my $message = "${buffer} -- ${name} :: $_[7]"; + if( weechat::config_get_plugin( "notify" ) ne "on" ) { + my $command = weechat::config_get_plugin( "command" ); + if( $command ne "" ) { + if( $command =~ /\$msg/ ) { + $command =~ s/\$msg/\'$message\'/; + } else { + $command = "$command '$message'"; + } + weechat::command( "", "/shell $command" ); + } else { + weechat::print("", $message_color); + } + } else { + weechat::print_date_tags("", 0, "notify_highlight", $message_color); + } + } + } + + return weechat::WEECHAT_RC_OK; +} diff --git a/.config/weechat/perl/autoload/highmon.pl b/.config/weechat/perl/autoload/highmon.pl deleted file mode 120000 index 2eb5e1e5..00000000 --- a/.config/weechat/perl/autoload/highmon.pl +++ /dev/null @@ -1 +0,0 @@ -../highmon.pl \ No newline at end of file diff --git a/.config/weechat/perl/autoload/highmon.pl b/.config/weechat/perl/autoload/highmon.pl new file mode 100644 index 00000000..f843cade --- /dev/null +++ b/.config/weechat/perl/autoload/highmon.pl @@ -0,0 +1,1154 @@ +# +# highmon.pl - Highlight Monitoring for weechat 0.3.0 +# +# Add 'Highlight Monitor' buffer/bar to log all highlights in one spot +# +# Usage: +# /highmon [help] | [monitor [channel [server]]] | [clean default|orphan|all] | clearbar +# Command wrapper for highmon commands +# +# /highmon clean default|orphan|all will clean the config section of default 'on' entries, +# channels you are no longer joined, or both +# +# /highmon clearbar will clear the contents of highmon's bar output +# +# /highmon monitor [channel] [server] is used to toggle a highlight monitoring on and off, this +# can be used in the channel buffer for the channel you wish to toggle, or be given +# with arguments e.g. /highmon monitor #weechat freenode +# +# /set plugins.var.perl.highmon.alignment +# The config setting "alignment" can be changed to; +# "channel", "schannel", "nchannel", "channel,nick", "schannel,nick", "nchannel,nick" +# to change how the monitor appears +# The 'channel' value will show: "#weechat" +# The 'schannel' value will show: "6" +# The 'nchannel' value will show: "6:#weechat" +# +# /set plugins.var.perl.highmon.short_names +# Setting this to 'on' will trim the network name from highmon, ala buffers.pl +# +# /set plugins.var.perl.highmon.merge_private +# Setting this to 'on' will merge private messages to highmon's display +# +# /set plugins.var.perl.highmon.color_buf +# This turns colored buffer names on or off, you can also set a single fixed color by using a weechat color name. +# This *must* be a valid color name, or weechat will likely do unexpected things :) +# +# /set plugins.var.perl.highmon.hotlist_show +# Setting this to 'on' will let the highmon buffer appear in hotlists +# (status bar/buffer.pl) +# +# /set plugins.var.perl.highmon.away_only +# Setting this to 'on' will only put messages in the highmon buffer when +# you set your status to away +# +# /set plugins.var.perl.highmon.logging +# Toggles logging status for highmon buffer (default: off) +# +# /set plugins.var.perl.highmon.output +# Changes where output method of highmon; takes either "bar" or "buffer" (default; buffer) +# /set plugins.var.perl.highmon.bar_lines +# Changes the amount of lines the output bar will hold. +# (Only appears once output has been set to bar, defaults to 10) +# /set plugins.var.perl.highmon.bar_scrolldown +# Toggles the bar scrolling at the bottom when new highlights are received +# (Only appears once output has been set to bar, defaults to off) +# +# /set plugins.var.perl.highmon.nick_prefix +# /set plugins.var.perl.highmon.nick_suffix +# Sets the prefix and suffix chars in the highmon buffer +# (Defaults to <> if nothing set, and blank if there is) +# +# servername.#channel +# servername is the internal name for the server (set when you use /server add) +# #channel is the channel name, (where # is whatever channel type that channel happens to be) +# +# Optional, set up tweaks; Hide the status and input lines on highmon +# +# /set weechat.bar.status.conditions "${window.buffer.full_name} != perl.highmon" +# /set weechat.bar.input.conditions "${window.buffer.full_name} != perl.highmon" +# + +# Bugs and feature requests at: https://github.com/KenjiE20/highmon + +# History: +# 2020-06-21, Sebastien Helleu : +# v2.7: make call to bar_new compatible with WeeChat >= 2.9 +# 2019-05-13, HubbeKing +# v2.6: -add: send "logger_backlog" signal on buffer open if logging is enabled +# 2014-08-16, KenjiE20 : +# v2.5: -add: clearbar command to clear bar output +# -add: firstrun output prompt to check the help text for set up hints as they were being missed +# and update hint for conditions to use eval +# -change: Make all outputs use the date callback for more accurate timestamps (thanks Germainz) +# 2013-12-04, KenjiE20 : +# v2.4: -add: Support for eval style colour codes in time format used for bar output +# 2013-10-22, KenjiE20 : +# v2.3.3.2: -fix: Typo in fix command +# 2013-10-10, KenjiE20 : +# v2.3.3.1: -fix: Typo in closed buffer warning +# 2013-10-07, KenjiE20 : +# v2.3.3: -add: Warning and fixer for accidental buffer closes +# 2013-01-15, KenjiE20 : +# v2.3.2: -fix: Let bar output use the string set in weechat's config option +# -add: github info +# 2012-04-15, KenjiE20 : +# v2.3.1: -fix: Colour tags in bar timestamp string +# 2012-02-28, KenjiE20 : +# v2.3: -feature: Added merge_private option to display private messages (default: off) +# -fix: Channel name colours now show correctly when set to on +# 2011-08-07, Sitaktif : +# v2.2.1: -feature: Add "bar_scrolldown" option to have the bar display the latest hl at anytime +# -fix: Set up bar-specific config at startup if 'output' is already configured as 'bar' +# 2010-12-22, KenjiE20 : +# v2.2: -change: Use API instead of config to find channel colours, ready for 0.3.4 and 256 colours +# 2010-12-13, idl0r & KenjiE20 : +# v2.1.3: -fix: perl errors caused by bar line counter +# -fix: Add command list to inbuilt help +# 2010-09-30, KenjiE20 : +# v2.1.2: -fix: logging config was not correctly toggling back on (thanks to sleo for noticing) +# -version sync w/ chanmon +# 2010-08-27, KenjiE20 : +# v2.1: -feature: Add 'nchannel' option to alignment to display buffer and name +# 2010-04-25, KenjiE20 : +# v2.0: Release as version 2.0 +# 2010-04-24, KenjiE20 : +# v1.9: Rewrite for v2.0 +# Bring feature set in line with chanmon 2.0 +# -code change: Made more subs to shrink the code down in places +# -fix: Stop highmon attempting to double load/hook +# -fix: Add version dependant check for away status +# 2010-01-25, KenjiE20 : +# v1.7: -fixture: Let highmon be aware of nick_prefix/suffix +# and allow custom prefix/suffix for chanmon buffer +# (Defaults to <> if nothing set, and blank if there is) +# (Thanks to m4v for these) +# 2009-09-07, KenjiE20 : +# v1.6: -feature: colored buffer names +# -change: version sync with chanmon +# 2009-09-05, KenjiE20 : +# v1.2: -fix: disable buffer highlight +# 2009-09-02, KenjiE20 : +# v.1.1.1 -change: Stop unsightly text block on '/help' +# 2009-08-10, KenjiE20 : +# v1.1: In-client help added +# 2009-08-02, KenjiE20 : +# v1.0: Initial Public Release + +# Copyright (c) 2009 by KenjiE20 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +@bar_lines = (); +@bar_lines_time = (); +# Replicate info earlier for in-client help + +$highmonhelp = weechat::color("bold")."/highmon [help] | [monitor [channel [server]]] | [clean default|orphan|all] | clearbar".weechat::color("-bold")." + Command wrapper for highmon commands + +".weechat::color("bold")."/highmon clean default|orphan|all".weechat::color("-bold")." will clean the config section of default 'on' entries, channels you are no longer joined, or both + +".weechat::color("bold")."/highmon clearbar".weechat::color("-bold")." will clear the contents of highmon's bar output + +".weechat::color("bold")."/highmon monitor [channel] [server]".weechat::color("-bold")." is used to toggle a highlight monitoring on and off, this can be used in the channel buffer for the channel you wish to toggle, or be given with arguments e.g. /highmon monitor #weechat freenode + +".weechat::color("bold")."/set plugins.var.perl.highmon.alignment".weechat::color("-bold")." + The config setting \"alignment\" can be changed to; + \"channel\", \"schannel\", \"nchannel\", \"channel,nick\", \"schannel,nick\", \"nchannel,nick\" + to change how the monitor appears + The 'channel' value will show: \"#weechat\" + The 'schannel' value will show: \"6\" + The 'nchannel' value will show: \"6:#weechat\" + +".weechat::color("bold")."/set plugins.var.perl.highmon.short_names".weechat::color("-bold")." + Setting this to 'on' will trim the network name from highmon, ala buffers.pl + +".weechat::color("bold")."/set plugins.var.perl.highmon.merge_private".weechat::color("-bold")." + Setting this to 'on' will merge private messages to highmon's display + +".weechat::color("bold")."/set plugins.var.perl.highmon.color_buf".weechat::color("-bold")." + This turns colored buffer names on or off, you can also set a single fixed color by using a weechat color name. + This ".weechat::color("bold")."must".weechat::color("-bold")." be a valid color name, or weechat will likely do unexpected things :) + +".weechat::color("bold")."/set plugins.var.perl.highmon.hotlist_show".weechat::color("-bold")." +Setting this to 'on' will let the highmon buffer appear in hotlists (status bar/buffer.pl) + +".weechat::color("bold")."/set plugins.var.perl.highmon.away_only".weechat::color("-bold")." +Setting this to 'on' will only put messages in the highmon buffer when you set your status to away + +".weechat::color("bold")."/set plugins.var.perl.highmon.logging".weechat::color("-bold")." + Toggles logging status for highmon buffer (default: off) + +".weechat::color("bold")."/set plugins.var.perl.highmon.output".weechat::color("-bold")." + Changes where output method of highmon; takes either \"bar\" or \"buffer\" (default; buffer) +".weechat::color("bold")."/set plugins.var.perl.highmon.bar_lines".weechat::color("-bold")." + Changes the amount of lines the output bar will hold. + (Only appears once output has been set to bar, defaults to 10) +".weechat::color("bold")."/set plugins.var.perl.highmon.bar_scrolldown".weechat::color("-bold")." + Toggles the bar scrolling at the bottom when new highlights are received + (Only appears once output has been set to bar, defaults to off) + +".weechat::color("bold")."/set plugins.var.perl.highmon.nick_prefix".weechat::color("-bold")." +".weechat::color("bold")."/set plugins.var.perl.highmon.nick_suffix".weechat::color("-bold")." + Sets the prefix and suffix chars in the highmon buffer + (Defaults to <> if nothing set, and blank if there is) + +".weechat::color("bold")."servername.#channel".weechat::color("-bold")." + servername is the internal name for the server (set when you use /server add) + #channel is the channel name, (where # is whatever channel type that channel happens to be) + +".weechat::color("bold")."Optional, set up tweaks;".weechat::color("-bold")." Hide the status and input lines on highmon + +".weechat::color("bold")."/set weechat.bar.status.conditions \"\${window.buffer.full_name} != perl.highmon\"".weechat::color("-bold")." +".weechat::color("bold")."/set weechat.bar.input.conditions \"\${window.buffer.full_name} != perl.highmon\"".weechat::color("-bold"); +# Print verbose help +sub print_help +{ + weechat::print("", "\t".weechat::color("bold")."Highmon Help".weechat::color("-bold")."\n\n"); + weechat::print("", "\t".$highmonhelp); + return weechat::WEECHAT_RC_OK; +} + +# Bar item build +sub highmon_bar_build +{ + # Get max lines + $max_lines = weechat::config_get_plugin("bar_lines"); + $max_lines = $max_lines ? $max_lines : 10; + $str = ''; + $align_num = 0; + $count = 0; + # Keep lines within max + while ($#bar_lines > $max_lines) + { + shift(@bar_lines); + shift(@bar_lines_time); + } + # So long as we have some lines, build a string + if (@bar_lines) + { + # Build loop + $sep = " ".weechat::config_string(weechat::config_get("weechat.look.prefix_suffix"))." "; + foreach(@bar_lines) + { + # Find max align needed + $prefix_num = (index(weechat::string_remove_color($_, ""), $sep)); + $align_num = $prefix_num if ($prefix_num > $align_num); + } + foreach(@bar_lines) + { + # Get align for this line + $prefix_num = (index(weechat::string_remove_color($_, ""), $sep)); + + # Make string + $str = $str.$bar_lines_time[$count]." ".(" " x ($align_num - $prefix_num)).$_."\n"; + # Increment count for sync with time list + $count++; + } + } + return $str; +} + +# Make a new bar +sub highmon_bar_open +{ + # Make the bar item + weechat::bar_item_new("highmon", "highmon_bar_build", ""); + + if (weechat::info_get("version_number", "") >= 0x02090000) + { + $highmon_bar = weechat::bar_new ("highmon", "off", 100, "root", "", "bottom", "vertical", "vertical", 0, 0, "default", "cyan", "default", "default", "on", "highmon"); + } + else + { + $highmon_bar = weechat::bar_new ("highmon", "off", 100, "root", "", "bottom", "vertical", "vertical", 0, 0, "default", "cyan", "default", "on", "highmon"); + } + + return weechat::WEECHAT_RC_OK; +} +# Close bar +sub highmon_bar_close +{ + # Find if bar exists + $highmon_bar = weechat::bar_search("highmon"); + # If is does, close it + if ($highmon_bar ne "") + { + weechat::bar_remove($highmon_bar); + } + + # Find if bar item exists + $highmon_bar_item = weechat::bar_item_search("highmon_bar"); + # If is does, close it + if ($highmon_bar_item ne "") + { + weechat::bar_remove($highmon_bar_item); + } + + @bar_lines = (); + return weechat::WEECHAT_RC_OK; +} + +# Make a new buffer +sub highmon_buffer_open +{ + # Search for pre-existing buffer + $highmon_buffer = weechat::buffer_search("perl", "highmon"); + + # Make a new buffer + if ($highmon_buffer eq "") + { + $highmon_buffer = weechat::buffer_new("highmon", "highmon_buffer_input", "", "highmon_buffer_close", ""); + } + + # Turn off notify, highlights + if ($highmon_buffer ne "") + { + if (weechat::config_get_plugin("hotlist_show") eq "off") + { + weechat::buffer_set($highmon_buffer, "notify", "0"); + } + weechat::buffer_set($highmon_buffer, "highlight_words", "-"); + weechat::buffer_set($highmon_buffer, "title", "Highlight Monitor"); + # Set no_log + if (weechat::config_get_plugin("logging") eq "off") + { + weechat::buffer_set($highmon_buffer, "localvar_set_no_log", "1"); + } + # send "logger_backlog" signal if logging is enabled to display backlog + if (weechat::config_get_plugin("logging") eq "on") + { + weechat::hook_signal_send("logger_backlog", weechat::WEECHAT_HOOK_SIGNAL_POINTER, $highmon_buffer) + } + } + return weechat::WEECHAT_RC_OK; +} +# Buffer input has no action +sub highmon_buffer_input +{ + return weechat::WEECHAT_RC_OK; +} +# Close up +sub highmon_buffer_close +{ + $highmon_buffer = ""; + # If user hasn't changed output style warn user + if (weechat::config_get_plugin("output") eq "buffer") + { + weechat::print("", "\tHighmon buffer has been closed but output is still set to buffer, unusual results may occur. To recreate the buffer use ".weechat::color("bold")."/highmon fix".weechat::color("-bold")); + } + return weechat::WEECHAT_RC_OK; +} + +# Highmon command wrapper +sub highmon_command_cb +{ + $data = $_[0]; + $buffer = $_[1]; + $args = $_[2]; + my $cmd = ''; + my $arg = ''; + + if ($args ne "") + { + # Split argument up + @arg_array = split(/ /,$args); + # Take first as command + $cmd = shift(@arg_array); + # Rebuild string to pass to subs + if (@arg_array) + { + $arg = join(" ", @arg_array); + } + } + + # Help command + if ($cmd eq "" || $cmd eq "help") + { + print_help(); + } + # /monitor command + elsif ($cmd eq "monitor") + { + highmon_toggle($data, $buffer, $arg); + } + # /highclean command + elsif ($cmd eq "clean") + { + highmon_config_clean($data, $buffer, $arg); + } + # clearbar command + elsif ($cmd eq "clearbar") + { + if (weechat::config_get_plugin("output") eq "bar") + { + @bar_lines = (); + weechat::bar_item_update("highmon"); + } + } + # Fix closed buffer + elsif ($cmd eq "fix") + { + if (weechat::config_get_plugin("output") eq "buffer" && $highmon_buffer eq "") + { + highmon_buffer_open(); + } + } + return weechat::WEECHAT_RC_OK; +} + +# Clean up config entries +sub highmon_config_clean +{ + $data = $_[0]; + $buffer = $_[1]; + $args = $_[2]; + + # Don't do anything if bad option given + if ($args ne "default" && $args ne "orphan" && $args ne "all") + { + weechat::print("", "\thighmon.pl: Unknown option"); + return weechat::WEECHAT_RC_OK; + } + + @chans = (); + # Load an infolist of highmon options + $infolist = weechat::infolist_get("option", "", "*highmon*"); + while (weechat::infolist_next($infolist)) + { + $name = weechat::infolist_string($infolist, "option_name"); + $name =~ s/perl\.highmon\.(\w*)\.([#&\+!])(.*)/$1.$2$3/; + if ($name =~ /^(.*)\.([#&\+!])(.*)$/) + { + $action = 0; + # Clean up all 'on's + if ($args eq "default" || $args eq "all") + { + # If value in config is "on" + if (weechat::config_get_plugin($name) eq "on") + { + # Unset and if successful flag as changed + $rc = weechat::config_unset_plugin($name); + if ($rc eq weechat::WEECHAT_CONFIG_OPTION_UNSET_OK_REMOVED) + { + $action = 1; + } + } + } + # Clean non joined + if ($args eq "orphan" || $args eq "all") + { + # If we can't find the buffer for this entry + if (weechat::buffer_search("irc", $name) eq "") + { + # Unset and if successful flag as changed + $rc = weechat::config_unset_plugin($name); + if ($rc eq weechat::WEECHAT_CONFIG_OPTION_UNSET_OK_REMOVED) + { + $action = 1; + } + } + } + # Add changed entry names to list + push (@chans, $name) if ($action); + } + } + weechat::infolist_free($infolist); + # If channels were cleaned from config + if (@chans) + { + # If only one entry + if (@chans == 1) + { + $str = "\thighmon.pl: Cleaned ".@chans." entry from the config:"; + } + else + { + $str = "\thighmon.pl: Cleaned ".@chans." entries from the config:"; + } + # Build a list of channels + foreach(@chans) + { + $str = $str." ".$_; + } + # Print what happened + weechat::print("",$str); + } + # Config seemed to be clean + else + { + weechat::print("", "\thighmon.pl: No entries removed"); + } + return weechat::WEECHAT_RC_OK; +} + +# Check config elements +sub highmon_config_init +{ + # First run default + if (!(weechat::config_is_set_plugin ("first_run"))) + { + if (weechat::config_get_plugin("first_run") ne "true") + { + weechat::print("", "\tThis appears to be the first time highmon has been run. For help and common set up hints see /highmon help"); + weechat::config_set_plugin("first_run", "true"); + } + } + # Alignment default + if (!(weechat::config_is_set_plugin ("alignment"))) + { + weechat::config_set_plugin("alignment", "channel"); + } + if (weechat::config_get_plugin("alignment") eq "") + { + weechat::config_set_plugin("alignment", "none"); + } + + # Short name default + if (!(weechat::config_is_set_plugin ("short_names"))) + { + weechat::config_set_plugin("short_names", "off"); + } + + # Coloured names default + if (!(weechat::config_is_set_plugin ("color_buf"))) + { + weechat::config_set_plugin("color_buf", "on"); + } + + # Hotlist show default + if (!(weechat::config_is_set_plugin ("hotlist_show"))) + { + weechat::config_set_plugin("hotlist_show", "off"); + } + + # Away only default + if (!(weechat::config_is_set_plugin ("away_only"))) + { + weechat::config_set_plugin("away_only", "off"); + } + + # highmon log default + if (!(weechat::config_is_set_plugin ("logging"))) + { + weechat::config_set_plugin("logging", "off"); + } + + # Output default + if (!(weechat::config_is_set_plugin ("output"))) + { + weechat::config_set_plugin("output", "buffer"); + } + + # Private message merging + if (!(weechat::config_is_set_plugin ("merge_private"))) + { + weechat::config_set_plugin("merge_private", "off"); + } + + # Set bar config in case output was set to "bar" before even changing the setting + if (weechat::config_get_plugin("output") eq "bar") + { + # Output bar lines default + if (!(weechat::config_is_set_plugin ("bar_lines"))) + { + weechat::config_set_plugin("bar_lines", "10"); + } + if (!(weechat::config_is_set_plugin ("bar_scrolldown"))) + { + weechat::config_set_plugin("bar_scrolldown", "off"); + } + } + + # Check for exisiting prefix/suffix chars, and setup accordingly + $prefix = weechat::config_get("irc.look.nick_prefix"); + $prefix = weechat::config_string($prefix); + $suffix = weechat::config_get("irc.look.nick_suffix"); + $suffix = weechat::config_string($suffix); + + if (!(weechat::config_is_set_plugin("nick_prefix"))) + { + if ($prefix eq "" && $suffix eq "") + { + weechat::config_set_plugin("nick_prefix", "<"); + } + else + { + weechat::config_set_plugin("nick_prefix", ""); + } + } + + if (!(weechat::config_is_set_plugin("nick_suffix"))) + { + if ($prefix eq "" && $suffix eq "") + { + weechat::config_set_plugin("nick_suffix", ">"); + } + else + { + weechat::config_set_plugin("nick_suffix", ""); + } + } +} + +# Get config updates +sub highmon_config_cb +{ + $point = $_[0]; + $name = $_[1]; + $value = $_[2]; + + $name =~ s/^plugins\.var\.perl\.highmon\.//; + + # Set logging on buffer + if ($name eq "logging") + { + # Search for pre-existing buffer + $highmon_buffer = weechat::buffer_search("perl", "highmon"); + if ($value eq "off") + { + weechat::buffer_set($highmon_buffer, "localvar_set_no_log", "1"); + } + else + { + weechat::buffer_set($highmon_buffer, "localvar_del_no_log", ""); + } + } + # Output changer + elsif ($name eq "output") + { + if ($value eq "bar") + { + # Search for pre-existing buffer + $highmon_buffer = weechat::buffer_search("perl", "highmon"); + # Close if it exists + if ($highmon_buffer ne "") + { + weechat::buffer_close($highmon_buffer) + } + + # Output bar lines default + if (!(weechat::config_is_set_plugin ("bar_lines"))) + { + weechat::config_set_plugin("bar_lines", "10"); + } + if (!(weechat::config_is_set_plugin ("bar_scrolldown"))) + { + weechat::config_set_plugin("bar_scrolldown", "off"); + } + # Make a bar if doesn't exist + highmon_bar_open(); + } + elsif ($value eq "buffer") + { + # If a bar exists, close it + highmon_bar_close(); + # Open buffer + highmon_buffer_open(); + } + + } + # Change if hotlist config changes + elsif ($name eq "hotlist_show") + { + # Search for pre-existing buffer + $highmon_buffer = weechat::buffer_search("perl", "highmon"); + if ($value eq "off" && $highmon_buffer) + { + weechat::buffer_set($highmon_buffer, "notify", "0"); + } + elsif ($value ne "off" && $highmon_buffer) + { + weechat::buffer_set($highmon_buffer, "notify", "3"); + } + } + elsif ($name eq "weechat.look.prefix_suffix") + { + if (weechat::config_get_plugin("output") eq "bar") + { + @bar_lines = (); + weechat::print("", "\thighmon: weechat.look.prefix_suffix changed, clearing highmon bar"); + weechat::bar_item_update("highmon"); + } + } + return weechat::WEECHAT_RC_OK; +} + +# Set up weechat hooks / commands +sub highmon_hook +{ + weechat::hook_print("", "", "", 0, "highmon_new_message", ""); + weechat::hook_command("highclean", "Highmon config clean up", "default|orphan|all", " default: Cleans all config entries with the default \"on\" value\n orphan: Cleans all config entries for channels you aren't currently joined\n all: Does both defaults and orphan", "default|orphan|all", "highmon_config_clean", ""); + + weechat::hook_command("highmon", "Highmon help", "[help] | [monitor [channel [server]]] | [clean default|orphan|all] | clearbar", " help: Print help on config options for highmon\n monitor: Toggles monitoring for a channel\n clean: Highmon config clean up (/highclean)\nclearbar: Clear Highmon bar", "help || monitor %(irc_channels) %(irc_servers) || clean default|orphan|all || clearbar", "highmon_command_cb", ""); + + weechat::hook_config("plugins.var.perl.highmon.*", "highmon_config_cb", ""); + weechat::hook_config("weechat.look.prefix_suffix", "highmon_config_cb", ""); +} + +# Main body, Callback for hook_print +sub highmon_new_message +{ + my $net = ""; + my $chan = ""; + my $nick = ""; + my $outstr = ""; + my $window_displayed = ""; + my $dyncheck = "0"; + +# DEBUG point +# $string = "\t"."0: ".$_[0]." 1: ".$_[1]." 2: ".$_[2]." 3: ".$_[3]." 4: ".$_[4]." 5: ".$_[5]." 6: ".$_[6]." 7: ".$_[7]; +# weechat::print("", "\t".$string); + + $cb_datap = $_[0]; + $cb_bufferp = $_[1]; + $cb_date = $_[2]; + $cb_tags = $_[3]; + $cb_disp = $_[4]; + $cb_high = $_[5]; + $cb_prefix = $_[6]; + $cb_msg = $_[7]; + + # Only work on highlighted messages or private message when enabled + if ($cb_high == "1" || (weechat::config_get_plugin("merge_private") eq "on" && $cb_tags =~ /notify_private/)) + { + # Pre bug #29618 (0.3.3) away detect + if (weechat::info_get("version_number", "") <= 0x00030200) + { + $away = ''; + # Get infolist for this server + $infolist = weechat::infolist_get("irc_server", "", weechat::buffer_get_string($cb_bufferp, "localvar_server")); + while (weechat::infolist_next($infolist)) + { + # Get away message is is_away is on + $away = weechat::infolist_string($infolist, "away_message") if (weechat::infolist_integer($infolist, "is_away")); + } + weechat::infolist_free($infolist); + } + # Post bug #29618 fix + else + { + $away = weechat::buffer_get_string($cb_bufferp, "localvar_away"); + } + if (weechat::config_get_plugin("away_only") ne "on" || ($away ne "")) + { + # Check buffer name is an IRC channel + $bufname = weechat::buffer_get_string($cb_bufferp, 'name'); + if ($bufname =~ /(.*)\.([#&\+!])(.*)/) + { + # Are we running on this channel + if (weechat::config_get_plugin($bufname) ne "off" && $cb_disp eq "1") + { + # Format nick + # Line isn't action or topic notify + if (!($cb_tags =~ /irc_action/) && !($cb_tags =~ /irc_topic/)) + { + # Strip nick colour + $uncolnick = weechat::string_remove_color($cb_prefix, ""); + # Format nick + $nick = " ".weechat::config_get_plugin("nick_prefix").weechat::color("chat_highlight").$uncolnick.weechat::color("reset").weechat::config_get_plugin("nick_suffix"); + } + # Topic line + elsif ($cb_tags =~ /irc_topic/) + { + $nick = " ".$cb_prefix.weechat::color("reset"); + } + # Action line + else + { + $uncolnick = weechat::string_remove_color($cb_prefix, ""); + $nick = weechat::color("chat_highlight").$uncolnick.weechat::color("reset"); + } + # Send to output + highmon_print ($cb_msg, $cb_bufferp, $nick, $cb_date, $cb_tags); + } + } + # Or is private message + elsif (weechat::config_get_plugin("merge_private") eq "on" && $cb_tags =~ /notify_private/) + { + # Strip nick colour + $uncolnick = weechat::buffer_get_string($cb_bufferp, 'short_name'); + # Format nick + $nick = " ".weechat::config_get_plugin("nick_prefix").weechat::color("chat_highlight").$uncolnick.weechat::color("reset").weechat::config_get_plugin("nick_suffix"); + #Send to output + highmon_print ($cb_msg, $cb_bufferp, $nick, $cb_date, $cb_tags); + } + } + } + return weechat::WEECHAT_RC_OK; +} + +# Output formatter and printer takes (msg bufpointer nick) +sub highmon_print +{ + $cb_msg = $_[0]; + my $cb_bufferp = $_[1] if ($_[1]); + my $nick = $_[2] if ($_[2]); + my $cb_date = $_[3] if ($_[3]); + my $cb_tags = $_[4] if ($_[4]); + + #Normal channel message + if ($cb_bufferp && $nick) + { + # Format buffer name + $bufname = format_buffer_name($cb_bufferp); + + # If alignment is #channel | nick msg + if (weechat::config_get_plugin("alignment") eq "channel") + { + $nick =~ s/\s(.*)/$1/; + # Build string + $outstr = $bufname."\t".$nick." ".$cb_msg; + } + # or if it is channel number | nick msg + elsif (weechat::config_get_plugin("alignment") eq "schannel") + { + $nick =~ s/\s(.*)/$1/; + # Use channel number instead + $bufname = weechat::color("chat_prefix_buffer").weechat::buffer_get_integer($cb_bufferp, 'number').weechat::color("reset"); + # Build string + $outstr = $bufname."\t".$nick." ".$cb_msg; + } + # or if it is number:#channel | nick msg + elsif (weechat::config_get_plugin("alignment") eq "nchannel") + { + $nick =~ s/\s(.*)/$1/; + # Place channel number in front of formatted name + $bufname = weechat::color("chat_prefix_buffer").weechat::buffer_get_integer($cb_bufferp, 'number').":".weechat::color("reset").$bufname; + # Build string + $outstr = $bufname."\t".$nick." ".$cb_msg; + } + # or if it is #channel nick | msg + elsif (weechat::config_get_plugin("alignment") eq "channel,nick") + { + # Build string + $outstr = $bufname.":".$nick."\t".$cb_msg; + } + # or if it is channel number nick | msg + elsif (weechat::config_get_plugin("alignment") eq "schannel,nick") + { + # Use channel number instead + $bufname = weechat::color("chat_prefix_buffer").weechat::buffer_get_integer($cb_bufferp, 'number').weechat::color("reset"); + # Build string + $outstr = $bufname.":".$nick."\t".$cb_msg; + } + # or if it is number:#channel nick | msg + elsif (weechat::config_get_plugin("alignment") eq "nchannel,nick") + { + # Place channel number in front of formatted name + $bufname = weechat::color("chat_prefix_buffer").weechat::buffer_get_integer($cb_bufferp, 'number').":".weechat::color("reset").$bufname; + # Build string + $outstr = $bufname.":".$nick."\t".$cb_msg; + } + # or finally | #channel nick msg + else + { + # Build string + $outstr = "\t".$bufname.":".$nick." ".$cb_msg; + } + } + # highmon channel toggle message + elsif ($cb_bufferp && !$nick) + { + # Format buffer name + $bufname = format_buffer_name($cb_bufferp); + + # If alignment is #channel * | * + if (weechat::config_get_plugin("alignment") =~ /channel/) + { + # If it's actually channel number * | * + if (weechat::config_get_plugin("alignment") =~ /schannel/) + { + # Use channel number instead + $bufname = weechat::color("chat_prefix_buffer").weechat::buffer_get_integer($cb_bufferp, 'number').weechat::color("reset"); + } + # Or if it's actually number:#channel * | * + if (weechat::config_get_plugin("alignment") =~ /nchannel/) + { + # Place channel number in front of formatted name + $bufname = weechat::color("chat_prefix_buffer").weechat::buffer_get_integer($cb_bufferp, 'number').":".weechat::color("reset").$bufname; + } + $outstr = $bufname."\t".$cb_msg; + } + # or if alignment is | * + else + { + $outstr = $bufname.": ".$cb_msg; + } + } + # highmon dynmon + elsif (!$cb_bufferp && !$nick) + { + $outstr = "\t".$cb_msg; + } + + # Send string to buffer + if (weechat::config_get_plugin("output") eq "buffer") + { + # Search for and confirm buffer + $highmon_buffer = weechat::buffer_search("perl", "highmon"); + # Print + if ($cb_date) + { + weechat::print_date_tags($highmon_buffer, $cb_date, $cb_tags, $outstr); + } + else + { + weechat::print($highmon_buffer, $outstr); + } + } + elsif (weechat::config_get_plugin("output") eq "bar") + { + # Add time string + use POSIX qw(strftime); + if ($cb_date) + { + $time = strftime(weechat::config_string(weechat::config_get("weechat.look.buffer_time_format")), localtime($cb_date)); + } + else + { + $time = strftime(weechat::config_string(weechat::config_get("weechat.look.buffer_time_format")), localtime); + } + # Colourise + if ($time =~ /\$\{(?:color:)?[\w,]+\}/) # Coloured string + { + while ($time =~ /\$\{(?:color:)?([\w,]+)\}/) + { + $color = weechat::color($1); + $time =~ s/\$\{(?:color:)?[\w,]+\}/$color/; + } + $time .= weechat::color("reset"); + } + else # Default string + { + $colour = weechat::color(weechat::config_string(weechat::config_get("weechat.color.chat_time_delimiters"))); + $reset = weechat::color("reset"); + $time =~ s/(\d*)(.)(\d*)/$1$colour$2$reset$3/g; + } + # Push updates to bar lists + push (@bar_lines_time, $time); + + # Change tab char + $delim = " ".weechat::color(weechat::config_string(weechat::config_get("weechat.color.chat_delimiters"))).weechat::config_string(weechat::config_get("weechat.look.prefix_suffix")).weechat::color("reset")." "; + $outstr =~ s/\t/$delim/; + + push (@bar_lines, $outstr); + # Trigger update + weechat::bar_item_update("highmon"); + + if (weechat::config_get_plugin("bar_scrolldown") eq "on") + { + weechat::command("", "/bar scroll highmon * ye") + } + } +} + +# Start the output display +sub highmon_start +{ + if (weechat::config_get_plugin("output") eq "buffer") + { + highmon_buffer_open(); + } + elsif (weechat::config_get_plugin("output") eq "bar") + { + highmon_bar_open(); + } +} + +# Takes two optional args (channel server), toggles monitoring on/off +sub highmon_toggle +{ + $data = $_[0]; + $buffer = $_[1]; + $args = $_[2]; + + # Check if we've been told what channel to act on + if ($args ne "") + { + # Split argument up + @arg_array = split(/ /,$args); + # Check if a server was given + if ($arg_array[1]) + { + # Find matching + $bufp = weechat::buffer_search("irc", $arg_array[1].".".$arg_array[0]); + } + else + { + $found_chans = 0; + # Loop through defined servers + $infolist = weechat::infolist_get("buffer", "", ""); + while (weechat::infolist_next($infolist)) + { + # Only interesting in IRC buffers + if (weechat::infolist_string($infolist, "plugin_name") eq "irc") + { + # Find buffers that maych + $sname = weechat::infolist_string($infolist, "short_name"); + if ($sname eq $arg_array[0]) + { + $found_chans++; + $bufp = weechat::infolist_pointer($infolist, "pointer"); + } + } + } + weechat::infolist_free($infolist); + # If the infolist found more than one channel, halt as we need to know which one + if ($found_chans > 1) + { + weechat::print("", "Channel name is not unique, please define server"); + return weechat::WEECHAT_RC_OK; + } + } + # Something didn't return right + if ($bufp eq "") + { + weechat::print("", "Could not find buffer"); + return weechat::WEECHAT_RC_OK; + } + } + else + { + # Get pointer from where we are + $bufp = weechat::current_buffer(); + } + # Get buffer name + $bufname = weechat::buffer_get_string($bufp, 'name'); + # Test if buffer is an IRC channel + if ($bufname =~ /(.*)\.([#&\+!])(.*)/) + { + if (weechat::config_get_plugin($bufname) eq "off") + { + # If currently off, set on + weechat::config_set_plugin($bufname, "on"); + + # Send to output formatter + highmon_print("Highlight Monitoring Enabled", $bufp); + return weechat::WEECHAT_RC_OK; + } + elsif (weechat::config_get_plugin($bufname) eq "on" || weechat::config_get_plugin($bufname) eq "") + { + # If currently on, set off + weechat::config_set_plugin($bufname, "off"); + + # Send to output formatter + highmon_print("Highlight Monitoring Disabled", $bufp); + return weechat::WEECHAT_RC_OK; + } + } +} + +# Takes a buffer pointer and returns a formatted name +sub format_buffer_name +{ + $cb_bufferp = $_[0]; + $bufname = weechat::buffer_get_string($cb_bufferp, 'name'); + + # Set colour from buffer name + if (weechat::config_get_plugin("color_buf") eq "on") + { + # Determine what colour to use + $color = weechat::info_get("irc_nick_color", $bufname); + if (!$color) + { + $color = 0; + @char_array = split(//,$bufname); + foreach $char (@char_array) + { + $color += ord($char); + } + $color %= 10; + $color = sprintf "weechat.color.chat_nick_color%02d", $color+1; + $color = weechat::config_get($color); + $color = weechat::config_string($color); + $color = weechat::color($color); + } + + # Private message just show network + if (weechat::config_get_plugin("merge_private") eq "on" && weechat::buffer_get_string($cb_bufferp, "localvar_type") eq "private") + { + $bufname = weechat::buffer_get_string($cb_bufferp, "localvar_server"); + } + # Format name to short or 'nicename' + elsif (weechat::config_get_plugin("short_names") eq "on") + { + $bufname = weechat::buffer_get_string($cb_bufferp, 'short_name'); + } + else + { + $bufname =~ s/(.*)\.([#&\+!])(.*)/$1$2$3/; + } + + # Build a coloured string + $bufname = $color.$bufname.weechat::color("reset"); + } + # User set colour name + elsif (weechat::config_get_plugin("color_buf") ne "off") + { + # Private message just show network + if (weechat::config_get_plugin("merge_private") eq "on" && weechat::buffer_get_string($cb_bufferp, "localvar_type") eq "private") + { + $bufname = weechat::buffer_get_string($cb_bufferp, "localvar_server"); + } + # Format name to short or 'nicename' + elsif (weechat::config_get_plugin("short_names") eq "on") + { + $bufname = weechat::buffer_get_string($cb_bufferp, 'short_name'); + } + else + { + $bufname =~ s/(.*)\.([#&\+!])(.*)/$1$2$3/; + } + + $color = weechat::config_get_plugin("color_buf"); + $bufname = weechat::color($color).$bufname.weechat::color("reset"); + } + # Stick with default colour + else + { + # Private message just show network + if (weechat::config_get_plugin("merge_private") eq "on" && weechat::buffer_get_string($cb_bufferp, "localvar_type") eq "private") + { + $bufname = weechat::buffer_get_string($cb_bufferp, "localvar_server"); + } + # Format name to short or 'nicename' + elsif (weechat::config_get_plugin("short_names") eq "on") + { + $bufname = weechat::buffer_get_string($cb_bufferp, 'short_name'); + } + else + { + $bufname =~ s/(.*)\.([#&\+!])(.*)/$1$2$3/; + } + } + + return $bufname; +} + +# Check result of register, and attempt to behave in a sane manner +if (!weechat::register("highmon", "KenjiE20", "2.7", "GPL3", "Highlight Monitor", "", "")) +{ + # Double load + weechat::print ("", "\tHighmon is already loaded"); + return weechat::WEECHAT_RC_OK; +} +else +{ + # Start everything + highmon_hook(); + highmon_config_init(); + highmon_start(); +} diff --git a/.config/weechat/perl/autoload/iset.pl b/.config/weechat/perl/autoload/iset.pl deleted file mode 120000 index 2746e0d8..00000000 --- a/.config/weechat/perl/autoload/iset.pl +++ /dev/null @@ -1 +0,0 @@ -../iset.pl \ No newline at end of file diff --git a/.config/weechat/perl/autoload/iset.pl b/.config/weechat/perl/autoload/iset.pl new file mode 100644 index 00000000..a4a8e35f --- /dev/null +++ b/.config/weechat/perl/autoload/iset.pl @@ -0,0 +1,1645 @@ +# +# Copyright (C) 2008-2017 Sebastien Helleu +# Copyright (C) 2010-2017 Nils Görs +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# Set WeeChat and plugins options interactively. +# +# History: +# +# 2020-06-21, Sebastien Helleu : +# version 4.4: make call to bar_new compatible with WeeChat >= 2.9 +# 2017-04-14, nils_2 +# version 4.3: add option "use_color" (https://github.com/weechat/scripts/issues/93) +# 2016-07-08, nils_2 +# version 4.2: add diff function +# 2016-02-06, Sebastien Helleu : +# version 4.1: remove debug print +# 2015-12-24, Sebastien Helleu : +# version 4.0: add support of parent options (inherited values in irc servers) +# with WeeChat >= 1.4 +# 2015-05-16, Sebastien Helleu : +# version 3.9: fix cursor position when editing an option with WeeChat >= 1.2 +# 2015-05-02, arza : +# version 3.8: don't append "null" to /set when setting an undefined setting +# 2015-05-01, nils_2 : +# version 3.7: fix two perl warnings (reported by t3chguy) +# 2014-09-30, arza : +# version 3.6: fix current line counter when options aren't found +# 2014-06-03, nils_2 : +# version 3.5: add new option "use_mute" +# 2014-01-30, stfn : +# version 3.4: add new options "color_value_diff" and "color_value_diff_selected" +# 2014-01-16, luz : +# version 3.3: fix bug with column alignment in iset buffer when option +# name contains unicode characters +# 2013-08-03, Sebastien Helleu : +# version 3.2: allow "q" as input in iset buffer to close it +# 2013-07-14, Sebastien Helleu : +# version 3.1: remove unneeded calls to iset_refresh() in mouse callback +# (faster mouse actions when lot of options are displayed), +# fix bug when clicking on a line after the last option displayed +# 2013-04-30, arza : +# version 3.0: simpler title, fix refresh on unset +# 2012-12-16, nils_2 : +# version 2.9: fix focus window with iset buffer on mouse click +# 2012-08-25, nils_2 : +# version 2.8: most important key and mouse bindings for iset buffer added to title-bar (idea The-Compiler) +# 2012-07-31, nils_2 : +# version 2.7: add combined option and value search (see /help iset) +# : add exact value search (see /help iset) +# : fix problem with metacharacter in value search +# : fix use of uninitialized value for unset option and reset value of option +# 2012-07-25, nils_2 : +# version 2.6: switch to iset buffer (if existing) when command /iset is called with arguments +# 2012-03-17, Sebastien Helleu : +# version 2.5: fix check of sections when creating config file +# 2012-03-09, Sebastien Helleu : +# version 2.4: fix reload of config file +# 2012-02-02, nils_2 : +# version 2.3: fixed: refresh problem with new search results and cursor was outside window. +# : add: new option "current_line" in title bar +# version 2.2: fixed: refresh error when toggling plugins description +# 2011-11-05, nils_2 : +# version 2.1: use own config file (iset.conf), fix own help color (used immediately) +# 2011-10-16, nils_2 : +# version 2.0: add support for left-mouse-button and more sensitive mouse gesture (for integer/color options) +# add help text for mouse support +# 2011-09-20, Sebastien Helleu : +# version 1.9: add mouse support, fix iset buffer, fix errors on first load under FreeBSD +# 2011-07-21, nils_2 : +# version 1.8: added: option "show_plugin_description" (alt+p) +# fixed: typos in /help iset (lower case for alt+'x' keys) +# 2011-05-29, nils_2 : +# version 1.7: added: version check for future needs +# added: new option (scroll_horiz) and usage of scroll_horiz function (weechat >= 0.3.6 required) +# fixed: help_bar did not pop up immediately using key-shortcut +# 2011-02-19, nils_2 : +# version 1.6: added: display of all possible values in help bar (show_help_extra_info) +# fixed: external user options never loaded when starting iset first time +# 2011-02-13, Sebastien Helleu : +# version 1.5: use new help format for command arguments +# 2011-02-03, nils_2 : +# version 1.4: fixed: restore value filter after /upgrade using buffer local variable. +# 2011-01-14, nils_2 : +# version 1.3: added function to search for values (option value_search_char). +# code optimization. +# 2010-12-26, Sebastien Helleu : +# version 1.2: improve speed of /upgrade when iset buffer is open, +# restore filter used after /upgrade using buffer local variable, +# use /iset filter argument if buffer is open. +# 2010-11-21, drubin : +# version 1.1.1: fix bugs with cursor position +# 2010-11-20, nils_2 : +# version 1.1: cursor position set to value +# 2010-08-03, Sebastien Helleu : +# version 1.0: move misplaced call to infolist_free() +# 2010-02-02, rettub : +# version 0.9: turn all the help stuff off if option 'show_help_bar' is 'off', +# new key binding - to toggle help_bar and help stuff on/off +# 2010-01-30, nils_2 : +# version 0.8: fix error when option does not exist +# 2010-01-24, Sebastien Helleu : +# version 0.7: display iset bar only on iset buffer +# 2010-01-22, nils_2 and drubin: +# version 0.6: add description in a bar, fix singular/plural bug in title bar, +# fix selected line when switching buffer +# 2009-06-21, Sebastien Helleu : +# version 0.5: fix bug with iset buffer after /upgrade +# 2009-05-02, Sebastien Helleu : +# version 0.4: sync with last API changes +# 2009-01-04, Sebastien Helleu : +# version 0.3: open iset buffer when /iset command is executed +# 2009-01-04, Sebastien Helleu : +# version 0.2: use null values for options, add colors, fix refresh bugs, +# use new keys to reset/unset options, sort options by name, +# display number of options in buffer's title +# 2008-11-05, Sebastien Helleu : +# version 0.1: first official version +# 2008-04-19, Sebastien Helleu : +# script creation + +use strict; + +my $PRGNAME = "iset"; +my $VERSION = "4.4"; +my $DESCR = "Interactive Set for configuration options"; +my $AUTHOR = "Sebastien Helleu "; +my $LICENSE = "GPL3"; +my $LANG = "perl"; +my $ISET_CONFIG_FILE_NAME = "iset"; + +my $iset_config_file; +my $iset_buffer = ""; +my $wee_version_number = 0; +my @iset_focus = (); +my @options_names = (); +my @options_parent_names = (); +my @options_types = (); +my @options_values = (); +my @options_default_values = (); +my @options_parent_values = (); +my @options_is_null = (); +my $option_max_length = 0; +my $current_line = 0; +my $filter = "*"; +my $description = ""; +my $options_name_copy = ""; +my $iset_filter_title = ""; +# search modes: 0 = index() on value, 1 = grep() on value, 2 = grep() on option, 3 = grep on option & value, 4 = diff all, 5 = diff parts +my $search_mode = 2; +my $search_value = ""; +my $help_text_keys = "alt + space: toggle, +/-: increase/decrease, enter: change, ir: reset, iu: unset, v: toggle help bar"; +my $help_text_mouse = "Mouse: left: select, right: toggle/set, right + drag left/right: increase/decrease"; +my %options_iset; + +my %mouse_keys = ("\@chat(perl.$PRGNAME):button1" => "hsignal:iset_mouse", + "\@chat(perl.$PRGNAME):button2*" => "hsignal:iset_mouse", + "\@chat(perl.$PRGNAME):wheelup" => "/repeat 5 /iset **up", + "\@chat(perl.$PRGNAME):wheeldown" => "/repeat 5 /iset **down"); + + +sub iset_title +{ + if ($iset_buffer ne "") + { + my $current_line_counter = ""; + if (weechat::config_boolean($options_iset{"show_current_line"}) == 1) + { + if (@options_names eq 0) + { + $current_line_counter = "0/"; + } + else + { + $current_line_counter = ($current_line + 1) . "/"; + } + } + my $show_filter = ""; + if ($search_mode eq 0) + { + $iset_filter_title = "(value) "; + $show_filter = $search_value; + if ( substr($show_filter,0,1) eq weechat::config_string($options_iset{"value_search_char"}) ) + { + $show_filter = substr($show_filter,1,length($show_filter)); + } + } + elsif ($search_mode eq 1) + { + $iset_filter_title = "(value) "; + $show_filter = "*".$search_value."*"; + } + elsif ($search_mode eq 2) + { + $iset_filter_title = ""; + $filter = "*" if ($filter eq ""); + $show_filter = $filter; + } + elsif ($search_mode == 4 or $search_mode == 5) + { + $iset_filter_title = "diff: "; + $show_filter = "all"; + $show_filter = $search_value if $search_mode == 5; + } + elsif ($search_mode eq 3) + { + $iset_filter_title = "(option) "; + $show_filter = $filter + .weechat::color("default") + ." / (value) " + .weechat::color("yellow") + ."*".$search_value."*"; + } + weechat::buffer_set($iset_buffer, "title", + $iset_filter_title + .weechat::color("yellow") + .$show_filter + .weechat::color("default")." | " + .$current_line_counter + .@options_names + ." | " + .$help_text_keys + ." | " + .$help_text_mouse); + } +} + +sub iset_create_filter +{ + $filter = $_[0]; + if ( $search_mode == 3 ) + { + my @cmd_array = split(/ /,$filter); + my $array_count = @cmd_array; + $filter = $cmd_array[0]; + $filter = $cmd_array[0] . " " . $cmd_array[1] if ( $array_count >2 ); + } + $filter = "$1.*" if ($filter =~ /f (.*)/); # search file + $filter = "*.$1.*" if ($filter =~ /s (.*)/); # search section + if ((substr($filter, 0, 1) ne "*") && (substr($filter, -1, 1) ne "*")) + { + $filter = "*".$filter."*"; + } + if ($iset_buffer ne "") + { + weechat::buffer_set($iset_buffer, "localvar_set_iset_filter", $filter); + } +} + +sub iset_buffer_input +{ + my ($data, $buffer, $string) = ($_[0], $_[1], $_[2]); + + # string begins with space? + return weechat::WEECHAT_RC_OK if (substr($string, 0, 1 ) eq " "); + + if ($string eq "q") + { + weechat::buffer_close($buffer); + return weechat::WEECHAT_RC_OK; + } + $search_value = ""; + my @cmd_array = split(/ /,$string); + my $array_count = @cmd_array; + my $string2 = substr($string, 0, 1); + if ($string2 eq weechat::config_string($options_iset{"value_search_char"}) + or (defined $cmd_array[0] and $cmd_array[0] eq weechat::config_string($options_iset{"value_search_char"}).weechat::config_string($options_iset{"value_search_char"})) ) + { + $search_mode = 1; + $search_value = substr($string, 1); + iset_get_values($search_value); + if ($iset_buffer ne "") + { + weechat::buffer_set($iset_buffer, "localvar_set_iset_search_value", $search_value); + } + } + # show all diff values + elsif ($string eq "d") + { + $search_mode = 4; +# iset_title(); + iset_create_filter("*"); + iset_get_options("*"); + } + elsif ( $array_count >= 2 and $cmd_array[0] eq "d") + { + $search_mode = 5; + $search_value = substr($cmd_array[1], 0); # cut value_search_char + $search_value = substr($cmd_array[2], 0) if ( $array_count > 2); # cut value_search_char + iset_create_filter($search_value); + iset_get_options($search_value); + + } + else + { + $search_mode = 2; + if ( $array_count >= 2 and $cmd_array[0] ne "f" or $cmd_array[0] ne "s" ) + { + if ( defined $cmd_array[1] and substr($cmd_array[1], 0, 1) eq weechat::config_string($options_iset{"value_search_char"}) + or defined $cmd_array[2] and substr($cmd_array[2], 0, 1) eq weechat::config_string($options_iset{"value_search_char"}) ) + { + $search_mode = 3; + $search_value = substr($cmd_array[1], 1); # cut value_search_char + $search_value = substr($cmd_array[2], 1) if ( $array_count > 2); # cut value_search_char + } + } + if ( $search_mode == 3) + { + iset_create_filter($string); + iset_get_options($search_value); + } + else + { + iset_create_filter($string); + iset_get_options(""); + } + } + weechat::buffer_set($iset_buffer, "localvar_set_iset_search_mode", $search_mode); + weechat::buffer_clear($buffer); + $current_line = 0; + iset_refresh(); + return weechat::WEECHAT_RC_OK; +} + +sub iset_buffer_close +{ + $iset_buffer = ""; + + return weechat::WEECHAT_RC_OK; +} + +sub iset_init +{ + $current_line = 0; + $iset_buffer = weechat::buffer_search($LANG, $PRGNAME); + if ($iset_buffer eq "") + { + $iset_buffer = weechat::buffer_new($PRGNAME, "iset_buffer_input", "", "iset_buffer_close", ""); + } + else + { + my $new_filter = weechat::buffer_get_string($iset_buffer, "localvar_iset_filter"); + $search_mode = weechat::buffer_get_string($iset_buffer, "localvar_iset_search_mode"); + $search_value = weechat::buffer_get_string($iset_buffer, "localvar_iset_search_value"); + $filter = $new_filter if ($new_filter ne ""); + } + if ($iset_buffer ne "") + { + weechat::buffer_set($iset_buffer, "type", "free"); + iset_title(); + weechat::buffer_set($iset_buffer, "key_bind_ctrl-L", "/iset **refresh"); + weechat::buffer_set($iset_buffer, "key_bind_meta2-A", "/iset **up"); + weechat::buffer_set($iset_buffer, "key_bind_meta2-B", "/iset **down"); + weechat::buffer_set($iset_buffer, "key_bind_meta2-23~", "/iset **left"); + weechat::buffer_set($iset_buffer, "key_bind_meta2-24~" , "/iset **right"); + weechat::buffer_set($iset_buffer, "key_bind_meta- ", "/iset **toggle"); + weechat::buffer_set($iset_buffer, "key_bind_meta-+", "/iset **incr"); + weechat::buffer_set($iset_buffer, "key_bind_meta--", "/iset **decr"); + weechat::buffer_set($iset_buffer, "key_bind_meta-imeta-r", "/iset **reset"); + weechat::buffer_set($iset_buffer, "key_bind_meta-imeta-u", "/iset **unset"); + weechat::buffer_set($iset_buffer, "key_bind_meta-ctrl-J", "/iset **set"); + weechat::buffer_set($iset_buffer, "key_bind_meta-ctrl-M", "/iset **set"); + weechat::buffer_set($iset_buffer, "key_bind_meta-meta2-1~", "/iset **scroll_top"); + weechat::buffer_set($iset_buffer, "key_bind_meta-meta2-4~", "/iset **scroll_bottom"); + weechat::buffer_set($iset_buffer, "key_bind_meta-v", "/iset **toggle_help"); + weechat::buffer_set($iset_buffer, "key_bind_meta-p", "/iset **toggle_show_plugin_desc"); + weechat::buffer_set($iset_buffer, "localvar_set_iset_filter", $filter); + weechat::buffer_set($iset_buffer, "localvar_set_iset_search_mode", $search_mode); + weechat::buffer_set($iset_buffer, "localvar_set_iset_search_value", $search_value); + } +} + +sub iset_get_options +{ + my $var_value = $_[0]; + $var_value = "" if (not defined $var_value); + $var_value = lc($var_value); + $search_value = $var_value; + @iset_focus = (); + @options_names = (); + @options_parent_names = (); + @options_types = (); + @options_values = (); + @options_default_values = (); + @options_parent_values = (); + @options_is_null = (); + $option_max_length = 0; + my %options_internal = (); + my $i = 0; + my $key; + my $iset_struct; + my %iset_struct; + + weechat::buffer_set($iset_buffer, "localvar_set_iset_search_value", $var_value) if ($search_mode == 3); + + my $infolist = weechat::infolist_get("option", "", $filter); + while (weechat::infolist_next($infolist)) + { + $key = sprintf("%08d", $i); + my $name = weechat::infolist_string($infolist, "full_name"); + my $parent_name = weechat::infolist_string($infolist, "parent_name"); + next if (weechat::config_boolean($options_iset{"show_plugin_description"}) == 0 and index ($name, "plugins.desc.") != -1); + my $type = weechat::infolist_string($infolist, "type"); + my $value = weechat::infolist_string($infolist, "value"); + my $default_value = weechat::infolist_string($infolist, "default_value"); + my $parent_value; + if ($parent_name && (($wee_version_number < 0x00040300) || (weechat::infolist_search_var($infolist, "parent_value")))) + { + $parent_value = weechat::infolist_string($infolist, "parent_value"); + } + my $is_null = weechat::infolist_integer($infolist, "value_is_null"); + + if ($search_mode == 3) + { + my $value = weechat::infolist_string($infolist, "value"); + if ( grep /\Q$var_value/,lc($value) ) + { + $options_internal{$name}{"parent_name"} = $parent_name; + $options_internal{$name}{"type"} = $type; + $options_internal{$name}{"value"} = $value; + $options_internal{$name}{"default_value"} = $default_value; + $options_internal{$name}{"parent_value"} = $parent_value; + $options_internal{$name}{"is_null"} = $is_null; + $option_max_length = length($name) if (length($name) > $option_max_length); + $iset_struct{$key} = $options_internal{$name}; + push(@iset_focus, $iset_struct{$key}); + } + } + # search for diff? + elsif ( $search_mode == 4 or $search_mode == 5) + { + if ($value ne $default_value ) + { + $options_internal{$name}{"parent_name"} = $parent_name; + $options_internal{$name}{"type"} = $type; + $options_internal{$name}{"value"} = $value; + $options_internal{$name}{"default_value"} = $default_value; + $options_internal{$name}{"parent_value"} = $parent_value; + $options_internal{$name}{"is_null"} = $is_null; + $option_max_length = length($name) if (length($name) > $option_max_length); + $iset_struct{$key} = $options_internal{$name}; + push(@iset_focus, $iset_struct{$key}); + } + } + else + { + $options_internal{$name}{"parent_name"} = $parent_name; + $options_internal{$name}{"type"} = $type; + $options_internal{$name}{"value"} = $value; + $options_internal{$name}{"default_value"} = $default_value; + $options_internal{$name}{"parent_value"} = $parent_value; + $options_internal{$name}{"is_null"} = $is_null; + $option_max_length = length($name) if (length($name) > $option_max_length); + $iset_struct{$key} = $options_internal{$name}; + push(@iset_focus, $iset_struct{$key}); + } + $i++; + } + weechat::infolist_free($infolist); + + foreach my $name (sort keys %options_internal) + { + push(@options_names, $name); + push(@options_parent_names, $options_internal{$name}{"parent_name"}); + push(@options_types, $options_internal{$name}{"type"}); + push(@options_values, $options_internal{$name}{"value"}); + push(@options_default_values, $options_internal{$name}{"default_value"}); + push(@options_parent_values, $options_internal{$name}{"parent_value"}); + push(@options_is_null, $options_internal{$name}{"is_null"}); + } +} + +sub iset_get_values +{ + my $var_value = $_[0]; + $var_value = lc($var_value); + if (substr($var_value,0,1) eq weechat::config_string($options_iset{"value_search_char"}) and $var_value ne weechat::config_string($options_iset{"value_search_char"})) + { + $var_value = substr($var_value,1,length($var_value)); + $search_mode = 0; + } + iset_search_values($var_value,$search_mode); + weechat::buffer_set($iset_buffer, "localvar_set_iset_search_mode", $search_mode); + weechat::buffer_set($iset_buffer, "localvar_set_iset_search_value", $var_value); + $search_value = $var_value; +} +sub iset_search_values +{ + my ($var_value,$search_mode) = ($_[0],$_[1]); + @options_names = (); + @options_parent_names = (); + @options_types = (); + @options_values = (); + @options_default_values = (); + @options_parent_values = (); + @options_is_null = (); + $option_max_length = 0; + my %options_internal = (); + my $i = 0; + my $infolist = weechat::infolist_get("option", "", "*"); + while (weechat::infolist_next($infolist)) + { + my $name = weechat::infolist_string($infolist, "full_name"); + my $parent_name = weechat::infolist_string($infolist, "parent_name"); + next if (weechat::config_boolean($options_iset{"show_plugin_description"}) == 0 and index ($name, "plugins.desc.") != -1); + my $type = weechat::infolist_string($infolist, "type"); + my $is_null = weechat::infolist_integer($infolist, "value_is_null"); + my $value = weechat::infolist_string($infolist, "value"); + my $default_value = weechat::infolist_string($infolist, "default_value"); + my $parent_value; + if ($parent_name && (($wee_version_number < 0x00040300) || (weechat::infolist_search_var($infolist, "parent_value")))) + { + $parent_value = weechat::infolist_string($infolist, "parent_value"); + } + if ($search_mode) + { + if ( grep /\Q$var_value/,lc($value) ) + { + $options_internal{$name}{"parent_name"} = $parent_name; + $options_internal{$name}{"type"} = $type; + $options_internal{$name}{"value"} = $value; + $options_internal{$name}{"default_value"} = $default_value; + $options_internal{$name}{"parent_value"} = $parent_value; + $options_internal{$name}{"is_null"} = $is_null; + $option_max_length = length($name) if (length($name) > $option_max_length); + } + } + else + { +# if ($value =~ /\Q$var_value/si) + if (lc($value) eq $var_value) + { + $options_internal{$name}{"parent_name"} = $parent_name; + $options_internal{$name}{"type"} = $type; + $options_internal{$name}{"value"} = $value; + $options_internal{$name}{"default_value"} = $default_value; + $options_internal{$name}{"parent_value"} = $parent_value; + $options_internal{$name}{"is_null"} = $is_null; + $option_max_length = length($name) if (length($name) > $option_max_length); + } + } + $i++; + } + weechat::infolist_free($infolist); + foreach my $name (sort keys %options_internal) + { + push(@options_names, $name); + push(@options_parent_names, $options_internal{$name}{"parent_name"}); + push(@options_types, $options_internal{$name}{"type"}); + push(@options_values, $options_internal{$name}{"value"}); + push(@options_default_values, $options_internal{$name}{"default_value"}); + push(@options_parent_values, $options_internal{$name}{"parent_value"}); + push(@options_is_null, $options_internal{$name}{"is_null"}); + } +} + +sub iset_refresh_line +{ + if ($iset_buffer ne "") + { + my $y = $_[0]; + if ($y <= $#options_names) + { + return if (! defined($options_types[$y])); + my $format = sprintf("%%s%%s%%s %%s %%-7s %%s %%s%%s%%s"); + my $padding; + if ($wee_version_number >= 0x00040200) + { + $padding = " " x ($option_max_length - weechat::strlen_screen($options_names[$y])); + } + else + { + $padding = " " x ($option_max_length - length($options_names[$y])); + } + my $around = ""; + $around = "\"" if ((!$options_is_null[$y]) && ($options_types[$y] eq "string")); + + my $color1 = weechat::color(weechat::config_color($options_iset{"color_option"})); + my $color2 = weechat::color(weechat::config_color($options_iset{"color_type"})); + my $color3 = ""; + my $color4 = ""; + if ($options_is_null[$y]) + { + $color3 = weechat::color(weechat::config_color($options_iset{"color_value_undef"})); + $color4 = weechat::color(weechat::config_color($options_iset{"color_value"})); + } + elsif ($options_values[$y] ne $options_default_values[$y]) + { + $color3 = weechat::color(weechat::config_color($options_iset{"color_value_diff"})); + } + else + { + $color3 = weechat::color(weechat::config_color($options_iset{"color_value"})); + } + if ($y == $current_line) + { + $color1 = weechat::color(weechat::config_color($options_iset{"color_option_selected"}).",".weechat::config_color($options_iset{"color_bg_selected"})); + $color2 = weechat::color(weechat::config_color($options_iset{"color_type_selected"}).",".weechat::config_color($options_iset{"color_bg_selected"})); + if ($options_is_null[$y]) + { + $color3 = weechat::color(weechat::config_color($options_iset{"color_value_undef_selected"}).",".weechat::config_color($options_iset{"color_bg_selected"})); + $color4 = weechat::color(weechat::config_color($options_iset{"color_value_selected"}).",".weechat::config_color($options_iset{"color_bg_selected"})); + } + elsif ($options_values[$y] ne $options_default_values[$y]) + { + $color3 = weechat::color(weechat::config_color($options_iset{"color_value_diff_selected"}).",".weechat::config_color($options_iset{"color_bg_selected"})); + } + else + { + $color3 = weechat::color(weechat::config_color($options_iset{"color_value_selected"}).",".weechat::config_color($options_iset{"color_bg_selected"})); + } + } + my $value = $options_values[$y]; + if (weechat::config_boolean($options_iset{"use_color"}) == 1 and $options_types[$y] eq "color") + { + $value = weechat::color($options_values[$y]) . $options_values[$y]; + } + if ($options_is_null[$y]) + { + $value = "null"; + if ($options_parent_names[$y]) + { + if (defined $options_parent_values[$y]) + { + my $around_parent = ""; + $around_parent = "\"" if ($options_types[$y] eq "string"); + $value .= $color1." -> ".$color4.$around_parent.$options_parent_values[$y].$around_parent; + } + else + { + $value .= $color1." -> ".$color3."null"; + } + } + } + my $strline = sprintf($format, + $color1, $options_names[$y], $padding, + $color2, $options_types[$y], + $color3, $around, $value, $around); + weechat::print_y($iset_buffer, $y, $strline); + } + } +} + +sub iset_refresh +{ + iset_title(); + if (($iset_buffer ne "") && ($#options_names >= 0)) + { + foreach my $y (0 .. $#options_names) + { + iset_refresh_line($y); + } + } + + weechat::bar_item_update("isetbar_help") if (weechat::config_boolean($options_iset{"show_help_bar"}) == 1); +} + +sub iset_full_refresh +{ + $iset_buffer = weechat::buffer_search($LANG, $PRGNAME); + if ($iset_buffer ne "") + { + weechat::buffer_clear($iset_buffer) unless defined $_[0]; # iset_full_refresh(1) does a full refresh without clearing buffer + # search for "*" in $filter. + if ($filter =~ m/\*/ and $search_mode == 2) + { + iset_get_options(""); + } + else + { + if ($search_mode == 0) + { + $search_value = "=" . $search_value; + iset_get_values($search_value); + } + elsif ($search_mode == 1) + { + iset_get_values($search_value); + } + elsif ($search_mode == 3) + { + iset_create_filter($filter); + iset_get_options($search_value); + } + } + if (weechat::config_boolean($options_iset{"show_plugin_description"}) == 1) + { + iset_set_current_line($current_line); + }else + { + $current_line = $#options_names if ($current_line > $#options_names); + } + iset_refresh(); + weechat::command($iset_buffer, "/window refresh"); + } +} + +sub iset_set_current_line +{ + my $new_current_line = $_[0]; + if ($new_current_line >= 0) + { + my $old_current_line = $current_line; + $current_line = $new_current_line; + $current_line = $#options_names if ($current_line > $#options_names); + if ($old_current_line != $current_line) + { + iset_refresh_line($old_current_line); + iset_refresh_line($current_line); + weechat::bar_item_update("isetbar_help") if (weechat::config_boolean($options_iset{"show_help_bar"}) == 1); + } + } +} + +sub iset_signal_window_scrolled_cb +{ + my ($data, $signal, $signal_data) = ($_[0], $_[1], $_[2]); + if ($iset_buffer ne "") + { + my $infolist = weechat::infolist_get("window", $signal_data, ""); + if (weechat::infolist_next($infolist)) + { + if (weechat::infolist_pointer($infolist, "buffer") eq $iset_buffer) + { + my $old_current_line = $current_line; + my $new_current_line = $current_line; + my $start_line_y = weechat::infolist_integer($infolist, "start_line_y"); + my $chat_height = weechat::infolist_integer($infolist, "chat_height"); + $new_current_line += $chat_height if ($new_current_line < $start_line_y); + $new_current_line -= $chat_height if ($new_current_line >= $start_line_y + $chat_height); + $new_current_line = $start_line_y if ($new_current_line < $start_line_y); + $new_current_line = $start_line_y + $chat_height - 1 if ($new_current_line >= $start_line_y + $chat_height); + iset_set_current_line($new_current_line); + } + } + weechat::infolist_free($infolist); + } + + return weechat::WEECHAT_RC_OK; +} + +sub iset_get_window_number +{ + if ($iset_buffer ne "") + { + my $window = weechat::window_search_with_buffer($iset_buffer); + return "-window ".weechat::window_get_integer ($window, "number")." " if ($window ne ""); + } + return ""; +} + +sub iset_check_line_outside_window +{ + if ($iset_buffer ne "") + { + undef my $infolist; + if ($wee_version_number >= 0x00030500) + { + my $window = weechat::window_search_with_buffer($iset_buffer); + $infolist = weechat::infolist_get("window", $window, "") if $window; + } + else + { + $infolist = weechat::infolist_get("window", "", "current"); + } + if ($infolist) + { + if (weechat::infolist_next($infolist)) + { + my $start_line_y = weechat::infolist_integer($infolist, "start_line_y"); + my $chat_height = weechat::infolist_integer($infolist, "chat_height"); + my $window_number = ""; + if ($wee_version_number >= 0x00030500) + { + $window_number = "-window ".weechat::infolist_integer($infolist, "number")." "; + } + if ($start_line_y > $current_line) + { + weechat::command($iset_buffer, "/window scroll ".$window_number."-".($start_line_y - $current_line)); + } + else + { + if ($start_line_y <= $current_line - $chat_height) + { + weechat::command($iset_buffer, "/window scroll ".$window_number."+".($current_line - $start_line_y - $chat_height + 1)); + + } + } + } + weechat::infolist_free($infolist); + } + } +} + +sub iset_get_option_name_index +{ + my $option_name = $_[0]; + my $index = 0; + while ($index <= $#options_names) + { + return -1 if ($options_names[$index] gt $option_name); + return $index if ($options_names[$index] eq $option_name); + $index++; + } + return -1; +} + +sub iset_refresh_option +{ + my $option_name = $_[0]; + my $index = $_[1]; + my $infolist = weechat::infolist_get("option", "", $option_name); + if ($infolist) + { + weechat::infolist_next($infolist); + if (weechat::infolist_fields($infolist)) + { + $options_parent_names[$index] = weechat::infolist_string($infolist, "parent_name"); + $options_types[$index] = weechat::infolist_string($infolist, "type"); + $options_values[$index] = weechat::infolist_string($infolist, "value"); + $options_default_values[$index] = weechat::infolist_string($infolist, "default_value"); + $options_is_null[$index] = weechat::infolist_integer($infolist, "value_is_null"); + $options_parent_values[$index] = undef; + if ($options_parent_names[$index] + && (($wee_version_number < 0x00040300) || (weechat::infolist_search_var($infolist, "parent_value")))) + { + $options_parent_values[$index] = weechat::infolist_string($infolist, "parent_value"); + } + iset_refresh_line($index); + iset_title() if ($option_name eq "iset.look.show_current_line"); + } + else + { + iset_full_refresh(1); # if not found, refresh fully without clearing buffer + weechat::print_y($iset_buffer, $#options_names + 1, ""); + } + weechat::infolist_free($infolist); + } +} + +sub iset_config_cb +{ + my ($data, $option_name, $value) = ($_[0], $_[1], $_[2]); + + if ($iset_buffer ne "") + { + return weechat::WEECHAT_RC_OK if (weechat::info_get("weechat_upgrading", "") eq "1"); + + my $index = iset_get_option_name_index($option_name); + if ($index >= 0) + { + # refresh info about changed option + iset_refresh_option($option_name, $index); + # refresh any other option having this changed option as parent + foreach my $i (0 .. $#options_names) + { + if ($options_parent_names[$i] eq $option_name) + { + iset_refresh_option($options_names[$i], $i); + } + } + } + else + { + iset_full_refresh() if ($option_name ne "weechat.bar.isetbar.hidden"); + } + } + + return weechat::WEECHAT_RC_OK; +} + +sub iset_set_option +{ + my ($option, $value) = ($_[0],$_[1]); + if (defined $option and defined $value) + { + $option = weechat::config_get($option); + weechat::config_option_set($option, $value, 1) if ($option ne ""); + } +} + +sub iset_reset_option +{ + my $option = $_[0]; + if (defined $option) + { + $option = weechat::config_get($option); + weechat::config_option_reset($option, 1) if ($option ne ""); + } +} + +sub iset_unset_option +{ + my $option = $_[0]; + if (defined $option) + { + $option = weechat::config_get($option); + weechat::config_option_unset($option) if ($option ne ""); + } +} + + +sub iset_cmd_cb +{ + my ($data, $buffer, $args) = ($_[0], $_[1], $_[2]); + my $filter_set = 0; +# $search_value = ""; + if (($args ne "") && (substr($args, 0, 2) ne "**")) + { + my @cmd_array = split(/ /,$args); + my $array_count = @cmd_array; + if (substr($args, 0, 1) eq weechat::config_string($options_iset{"value_search_char"}) + or (defined $cmd_array[0] and $cmd_array[0] eq weechat::config_string($options_iset{"value_search_char"}).weechat::config_string($options_iset{"value_search_char"})) ) + { + $search_mode = 1; + my $search_value = substr($args, 1); # cut value_search_char + if ($iset_buffer ne "") + { + weechat::buffer_clear($iset_buffer); + weechat::command($iset_buffer, "/window refresh"); + } + weechat::buffer_set($iset_buffer, "localvar_set_iset_search_mode", $search_mode); + weechat::buffer_set($iset_buffer, "localvar_set_iset_search_value", $search_value); + iset_init(); + iset_get_values($search_value); + iset_refresh(); + weechat::buffer_set($iset_buffer, "display", "1"); +# $filter = $var_value; + return weechat::WEECHAT_RC_OK; + } + else + { + # f/s option =value + # option =value + $search_mode = 2; # grep on option + if ( $array_count >= 2 and $cmd_array[0] ne "f" or $cmd_array[0] ne "s") + { + if ( defined $cmd_array[1] and substr($cmd_array[1], 0, 1) eq weechat::config_string($options_iset{"value_search_char"}) + or defined $cmd_array[2] and substr($cmd_array[2], 0, 1) eq weechat::config_string($options_iset{"value_search_char"}) ) + { + $search_mode = 3; # grep on option and value + $search_value = substr($cmd_array[1], 1); # cut value_search_char + $search_value = substr($cmd_array[2], 1) if ( $array_count > 2); # cut value_search_char + } + } + + # show all diff values + if ( $args eq "d") + { + $search_mode = 4; + $search_value = "*"; + $args = $search_value; + } + if ( $array_count >= 2 and $cmd_array[0] eq "d") + { + $search_mode = 5; + $search_value = substr($cmd_array[1], 0); # cut value_search_char + $search_value = substr($cmd_array[2], 0) if ( $array_count > 2); # cut value_search_char + $args = $search_value; + } + + iset_create_filter($args); + $filter_set = 1; + my $ptrbuf = weechat::buffer_search($LANG, $PRGNAME); + + if ($ptrbuf eq "") + { + iset_init(); + iset_get_options($search_value); + iset_full_refresh(); + weechat::buffer_set(weechat::buffer_search($LANG, $PRGNAME), "display", "1"); + weechat::buffer_set($iset_buffer, "localvar_set_iset_search_value", $search_value); + weechat::buffer_set($iset_buffer, "localvar_set_iset_search_mode", $search_mode); + return weechat::WEECHAT_RC_OK; + } + else + { + iset_get_options($search_value); + iset_full_refresh(); + weechat::buffer_set($ptrbuf, "display", "1"); + } + } + weechat::buffer_set($iset_buffer, "localvar_set_iset_search_mode", $search_mode); + weechat::buffer_set($iset_buffer, "localvar_set_iset_search_value", $search_value); + } + if ($iset_buffer eq "") + { + iset_init(); + iset_get_options(""); + iset_refresh(); + } + else + { +# iset_get_options($search_value); + iset_full_refresh() if ($filter_set); + } + + if ($args eq "") + { + weechat::buffer_set($iset_buffer, "display", "1"); + } + else + { + if ($args eq "**refresh") + { + iset_full_refresh(); + } + if ($args eq "**up") + { + if ($current_line > 0) + { + $current_line--; + iset_refresh_line($current_line + 1); + iset_refresh_line($current_line); + iset_check_line_outside_window(); + } + } + if ($args eq "**down") + { + if ($current_line < $#options_names) + { + $current_line++; + iset_refresh_line($current_line - 1); + iset_refresh_line($current_line); + iset_check_line_outside_window(); + } + } + if ($args eq "**left" && $wee_version_number >= 0x00030600) + { + weechat::command($iset_buffer, "/window scroll_horiz ".iset_get_window_number()."-".weechat::config_integer($options_iset{"scroll_horiz"})."%"); + } + if ($args eq "**right" && $wee_version_number >= 0x00030600) + { + weechat::command($iset_buffer, "/window scroll_horiz ".iset_get_window_number().weechat::config_integer($options_iset{"scroll_horiz"})."%"); + } + if ($args eq "**scroll_top") + { + my $old_current_line = $current_line; + $current_line = 0; + iset_refresh_line ($old_current_line); + iset_refresh_line ($current_line); + iset_title(); + weechat::command($iset_buffer, "/window scroll_top ".iset_get_window_number()); + } + if ($args eq "**scroll_bottom") + { + my $old_current_line = $current_line; + $current_line = $#options_names; + iset_refresh_line ($old_current_line); + iset_refresh_line ($current_line); + iset_title(); + weechat::command($iset_buffer, "/window scroll_bottom ".iset_get_window_number()); + } + if ($args eq "**toggle") + { + if ($options_types[$current_line] eq "boolean") + { + iset_set_option($options_names[$current_line], "toggle"); + } + } + if ($args eq "**incr") + { + if (($options_types[$current_line] eq "integer") + || ($options_types[$current_line] eq "color")) + { + iset_set_option($options_names[$current_line], "++1"); + } + } + if ($args eq "**decr") + { + if (($options_types[$current_line] eq "integer") + || ($options_types[$current_line] eq "color")) + { + iset_set_option($options_names[$current_line], "--1"); + } + } + if ($args eq "**reset") + { + iset_reset_option($options_names[$current_line]); + } + if ($args eq "**unset") + { + iset_unset_option($options_names[$current_line]); + } + if ($args eq "**toggle_help") + { + if (weechat::config_boolean($options_iset{"show_help_bar"}) == 1) + { + weechat::config_option_set($options_iset{"show_help_bar"},0,1); + iset_show_bar(0); + } + else + { + weechat::config_option_set($options_iset{"show_help_bar"},1,1); + iset_show_bar(1); + } + } + if ($args eq "**toggle_show_plugin_desc") + { + if (weechat::config_boolean($options_iset{"show_plugin_description"}) == 1) + { + weechat::config_option_set($options_iset{"show_plugin_description"},0,1); + iset_full_refresh(); + iset_check_line_outside_window(); + iset_title(); + } + else + { + weechat::config_option_set($options_iset{"show_plugin_description"},1,1); + iset_full_refresh(); + iset_check_line_outside_window(); + iset_title(); + } + } + if ($args eq "**set") + { + my $quote = ""; + my $value = $options_values[$current_line]; + if ($options_is_null[$current_line]) + { + $value = ""; + } + else + { + $quote = "\"" if ($options_types[$current_line] eq "string"); + } + $value = " ".$quote.$value.$quote if ($value ne "" or $quote ne ""); + + my $set_command = "/set"; + my $start_index = 5; + if (weechat::config_boolean($options_iset{"use_mute"}) == 1) + { + $set_command = "/mute ".$set_command; + $start_index += 11; + } + $set_command = $set_command." ".$options_names[$current_line].$value; + my $pos_space = index($set_command, " ", $start_index); + if ($pos_space < 0) + { + $pos_space = 9999; + } + else + { + $pos_space = $pos_space + 1; + $pos_space = $pos_space + 1 if ($quote ne ""); + } + weechat::buffer_set($iset_buffer, "input", $set_command); + weechat::buffer_set($iset_buffer, "input_pos", "".$pos_space); + } + } + weechat::bar_item_update("isetbar_help") if (weechat::config_boolean($options_iset{"show_help_bar"}) == 1); + return weechat::WEECHAT_RC_OK; +} + +sub iset_get_help +{ + my ($redraw) = ($_[0]); + + return '' if (weechat::config_boolean($options_iset{"show_help_bar"}) == 0); + + if (not defined $options_names[$current_line]) + { + return "No option selected. Set a new filter using command line (use '*' to see all options)"; + } + if ($options_name_copy eq $options_names[$current_line] and not defined $redraw) + { + return $description; + } + $options_name_copy = $options_names[$current_line]; + my $optionlist =""; + $optionlist = weechat::infolist_get("option", "", $options_names[$current_line]); + weechat::infolist_next($optionlist); + my $full_name = weechat::infolist_string($optionlist,"full_name"); + my $option_desc = ""; + my $option_default_value = ""; + my $option_range = ""; + my $possible_values = ""; + my $re = qq(\Q$full_name); + if (grep (/^$re$/,$options_names[$current_line])) + { + $option_desc = weechat::infolist_string($optionlist, "description_nls"); + $option_desc = weechat::infolist_string($optionlist, "description") if ($option_desc eq ""); + $option_desc = "No help found" if ($option_desc eq ""); + $option_default_value = weechat::infolist_string($optionlist, "default_value"); + $possible_values = weechat::infolist_string($optionlist, "string_values") if (weechat::infolist_string($optionlist, "string_values") ne ""); + if ((weechat::infolist_string($optionlist, "type") eq "integer") && ($possible_values eq "")) + { + $option_range = weechat::infolist_integer($optionlist, "min") + ." .. ".weechat::infolist_integer($optionlist, "max"); + } + } + weechat::infolist_free($optionlist); + iset_title(); + + $description = weechat::color(weechat::config_color($options_iset{"color_help_option_name"})).$options_names[$current_line] + .weechat::color("bar_fg").": " + .weechat::color(weechat::config_color($options_iset{"color_help_text"})).$option_desc; + + # show additional infos like default value and possible values + + if (weechat::config_boolean($options_iset{"show_help_extra_info"}) == 1) + { + $description .= + weechat::color("bar_delim")." [" + .weechat::color("bar_fg")."default: " + .weechat::color("bar_delim")."\"" + .weechat::color(weechat::config_color($options_iset{"color_help_default_value"})).$option_default_value + .weechat::color("bar_delim")."\""; + if ($option_range ne "") + { + $description .= weechat::color("bar_fg").", values: ".$option_range; + } + if ($possible_values ne "") + { + $possible_values =~ s/\|/", "/g; # replace '|' to '", "' + $description .= weechat::color("bar_fg").", values: ". "\"" . $possible_values . "\""; + + } + $description .= weechat::color("bar_delim")."]"; + } + return $description; +} + +sub iset_check_condition_isetbar_cb +{ + my ($data, $modifier, $modifier_data, $string) = ($_[0], $_[1], $_[2], $_[3]); + my $buffer = weechat::window_get_pointer($modifier_data, "buffer"); + if ($buffer ne "") + { + if ((weechat::buffer_get_string($buffer, "plugin") eq $LANG) + && (weechat::buffer_get_string($buffer, "name") eq $PRGNAME)) + { + return "1"; + } + } + return "0"; +} + +sub iset_show_bar +{ + my $show = $_[0]; + my $barhidden = weechat::config_get("weechat.bar.isetbar.hidden"); + if ($barhidden) + { + if ($show) + { + if (weechat::config_boolean($options_iset{"show_help_bar"}) == 1) + { + if (weechat::config_boolean($barhidden)) + { + weechat::config_option_set($barhidden, 0, 1); + } + } + } + else + { + if (!weechat::config_boolean($barhidden)) + { + weechat::config_option_set($barhidden, 1, 1); + } + } + } +} + +sub iset_signal_buffer_switch_cb +{ + my $buffer_pointer = $_[2]; + my $show_bar = 0; + $show_bar = 1 if (weechat::buffer_get_integer($iset_buffer, "num_displayed") > 0); + iset_show_bar($show_bar); + iset_check_line_outside_window() if ($buffer_pointer eq $iset_buffer); + return weechat::WEECHAT_RC_OK; +} + +sub iset_item_cb +{ + return iset_get_help(); +} + +sub iset_upgrade_ended +{ + iset_full_refresh(); +} + +sub iset_end +{ + # when script is unloaded, we hide bar + iset_show_bar(0); +} + +# -------------------------------[ mouse support ]------------------------------------- + +sub hook_focus_iset_cb +{ + my %info = %{$_[1]}; + my $bar_item_line = int($info{"_bar_item_line"}); + undef my $hash; + if (($info{"_buffer_name"} eq $PRGNAME) && $info{"_buffer_plugin"} eq $LANG && ($bar_item_line >= 0) && ($bar_item_line <= $#iset_focus)) + { + $hash = $iset_focus[$bar_item_line]; + } + else + { + $hash = {}; + my $hash_focus = $iset_focus[0]; + foreach my $key (keys %$hash_focus) + { + $hash->{$key} = "?"; + } + } + return $hash; +} + +# _chat_line_y contains selected line +sub iset_hsignal_mouse_cb +{ + my ($data, $signal, %hash) = ($_[0], $_[1], %{$_[2]}); + + return weechat::WEECHAT_RC_OK unless (@options_types); + + if ($hash{"_buffer_name"} eq $PRGNAME && ($hash{"_buffer_plugin"} eq $LANG)) + { + if ($hash{"_key"} eq "button1") + { + iset_set_current_line($hash{"_chat_line_y"}); + } + elsif ($hash{"_key"} eq "button2") + { + if ($options_types[$hash{"_chat_line_y"}] eq "boolean") + { + iset_set_option($options_names[$hash{"_chat_line_y"}], "toggle"); + iset_set_current_line($hash{"_chat_line_y"}); + } + elsif ($options_types[$hash{"_chat_line_y"}] eq "string") + { + iset_set_current_line($hash{"_chat_line_y"}); + weechat::command("", "/$PRGNAME **set"); + } + } + elsif ($hash{"_key"} eq "button2-gesture-left" or $hash{"_key"} eq "button2-gesture-left-long") + { + if ($options_types[$hash{"_chat_line_y"}] eq "integer" or ($options_types[$hash{"_chat_line_y"}] eq "color")) + { + iset_set_current_line($hash{"_chat_line_y"}); + my $distance = distance($hash{"_chat_line_x"},$hash{"_chat_line_x2"}); + weechat::command("", "/repeat $distance /$PRGNAME **decr"); + } + } + elsif ($hash{"_key"} eq "button2-gesture-right" or $hash{"_key"} eq "button2-gesture-right-long") + { + if ($options_types[$hash{"_chat_line_y"}] eq "integer" or ($options_types[$hash{"_chat_line_y"}] eq "color")) + { + iset_set_current_line($hash{"_chat_line_y"}); + my $distance = distance($hash{"_chat_line_x"},$hash{"_chat_line_x2"}); + weechat::command("", "/repeat $distance /$PRGNAME **incr"); + } + } + } + window_switch(); +} + +sub window_switch +{ + my $current_window = weechat::current_window(); + my $dest_window = weechat::window_search_with_buffer(weechat::buffer_search("perl","iset")); + return 0 if ($dest_window eq "" or $current_window eq $dest_window); + + my $infolist = weechat::infolist_get("window", $dest_window, ""); + weechat::infolist_next($infolist); + my $number = weechat::infolist_integer($infolist, "number"); + weechat::infolist_free($infolist); + weechat::command("","/window " . $number); +} + +sub distance +{ + my ($x1,$x2) = ($_[0], $_[1]); + my $distance; + $distance = $x1 - $x2; + $distance = abs($distance); + if ($distance > 0) + { + use integer; + $distance = $distance / 3; + $distance = 1 if ($distance == 0); + } + elsif ($distance == 0) + { + $distance = 1; + } + return $distance; +} + +# -----------------------------------[ config ]--------------------------------------- + +sub iset_config_init +{ + $iset_config_file = weechat::config_new($ISET_CONFIG_FILE_NAME,"iset_config_reload_cb",""); + return if ($iset_config_file eq ""); + + # section "color" + my $section_color = weechat::config_new_section($iset_config_file,"color", 0, 0, "", "", "", "", "", "", "", "", "", ""); + if ($section_color eq "") + { + weechat::config_free($iset_config_file); + return; + } + $options_iset{"color_option"} = weechat::config_new_option( + $iset_config_file, $section_color, + "option", "color", "Color for option name in iset buffer", "", 0, 0, + "default", "default", 0, "", "", "full_refresh_cb", "", "", ""); + $options_iset{"color_option_selected"} = weechat::config_new_option( + $iset_config_file, $section_color, + "option_selected", "color", "Color for selected option name in iset buffer", "", 0, 0, + "white", "white", 0, "", "", "full_refresh_cb", "", "", ""); + $options_iset{"color_type"} = weechat::config_new_option( + $iset_config_file, $section_color, + "type", "color", "Color for option type (integer, boolean, string)", "", 0, 0, + "brown", "brown", 0, "", "", "full_refresh_cb", "", "", ""); + $options_iset{"color_type_selected"} = weechat::config_new_option( + $iset_config_file, $section_color, + "type_selected", "color", "Color for selected option type (integer, boolean, string)", "", 0, 0, + "yellow", "yellow", 0, "", "", "full_refresh_cb", "", "", ""); + $options_iset{"color_value"} = weechat::config_new_option( + $iset_config_file, $section_color, + "value", "color", "Color for option value", "", 0, 0, + "cyan", "cyan", 0, "", "", "full_refresh_cb", "", "", ""); + $options_iset{"color_value_selected"} = weechat::config_new_option( + $iset_config_file, $section_color, + "value_selected", "color", "Color for selected option value", "", 0, 0, + "lightcyan", "lightcyan", 0, "", "", "full_refresh_cb", "", "", ""); + $options_iset{"color_value_diff"} = weechat::config_new_option( + $iset_config_file, $section_color, + "value_diff", "color", "Color for option value different from default", "", 0, 0, + "magenta", "magenta", 0, "", "", "full_refresh_cb", "", "", ""); + $options_iset{"color_value_diff_selected"} = weechat::config_new_option( + $iset_config_file, $section_color, + "value_diff_selected", "color", "Color for selected option value different from default", "", 0, 0, + "lightmagenta", "lightmagenta", 0, "", "", "full_refresh_cb", "", "", ""); + $options_iset{"color_value_undef"} = weechat::config_new_option( + $iset_config_file, $section_color, + "value_undef", "color", "Color for option value undef", "", 0, 0, + "green", "green", 0, "", "", "full_refresh_cb", "", "", ""); + $options_iset{"color_value_undef_selected"} = weechat::config_new_option( + $iset_config_file, $section_color, + "value_undef_selected", "color", "Color for selected option value undef", "", 0, 0, + "lightgreen", "lightgreen", 0, "", "", "full_refresh_cb", "", "", ""); + $options_iset{"color_bg_selected"} = weechat::config_new_option( + $iset_config_file, $section_color, + "bg_selected", "color", "Background color for current selected option", "", 0, 0, + "red", "red", 0, "", "", "full_refresh_cb", "", "", ""); + $options_iset{"color_help_option_name"} = weechat::config_new_option( + $iset_config_file, $section_color, + "help_option_name", "color", "Color for option name in help-bar", "", 0, 0, + "white", "white", 0, "", "", "bar_refresh", "", "", ""); + $options_iset{"color_help_text"} = weechat::config_new_option( + $iset_config_file, $section_color, + "help_text", "color", "Color for option description in help-bar", "", 0, 0, + "default", "default", 0, "", "", "bar_refresh", "", "", ""); + $options_iset{"color_help_default_value"} = weechat::config_new_option( + $iset_config_file, $section_color, + "help_default_value", "color", "Color for default option value in help-bar", "", 0, 0, + "green", "green", 0, "", "", "bar_refresh", "", "", ""); + + # section "help" + my $section_help = weechat::config_new_section($iset_config_file,"help", 0, 0, "", "", "", "", "", "", "", "", "", ""); + if ($section_help eq "") + { + weechat::config_free($iset_config_file); + return; + } + $options_iset{"show_help_bar"} = weechat::config_new_option( + $iset_config_file, $section_help, + "show_help_bar", "boolean", "Show help bar", "", 0, 0, + "on", "on", 0, "", "", "toggle_help_cb", "", "", ""); + $options_iset{"show_help_extra_info"} = weechat::config_new_option( + $iset_config_file, $section_help, + "show_help_extra_info", "boolean", "Show additional information in help bar (default value, max./min. value) ", "", 0, 0, + "on", "on", 0, "", "", "", "", "", ""); + $options_iset{"show_plugin_description"} = weechat::config_new_option( + $iset_config_file, $section_help, + "show_plugin_description", "boolean", "Show plugin description in iset buffer", "", 0, 0, + "off", "off", 0, "", "", "full_refresh_cb", "", "", ""); + + # section "look" + my $section_look = weechat::config_new_section($iset_config_file, "look", 0, 0, "", "", "", "", "", "", "", "", "", ""); + if ($section_look eq "") + { + weechat::config_free($iset_config_file); + return; + } + $options_iset{"value_search_char"} = weechat::config_new_option( + $iset_config_file, $section_look, + "value_search_char", "string", "Trigger char to tell iset to search for value instead of option (for example: =red)", "", 0, 0, + "=", "=", 0, "", "", "", "", "", ""); + $options_iset{"scroll_horiz"} = weechat::config_new_option( + $iset_config_file, $section_look, + "scroll_horiz", "integer", "scroll content of iset buffer n%", "", 1, 100, + "10", "10", 0, "", "", "", "", "", ""); + $options_iset{"show_current_line"} = weechat::config_new_option( + $iset_config_file, $section_look, + "show_current_line", "boolean", "show current line in title bar.", "", 0, 0, + "on", "on", 0, "", "", "", "", "", ""); + $options_iset{"use_mute"} = weechat::config_new_option( + $iset_config_file, $section_look, + "use_mute", "boolean", "/mute command will be used in input bar", "", 0, 0, + "off", "off", 0, "", "", "", "", "", ""); + $options_iset{"use_color"} = weechat::config_new_option( + $iset_config_file, $section_look, + "use_color", "boolean", "display the color value in the corresponding color", "", 0, 0, + "off", "off", 0, "", "", "full_refresh_cb", "", "", ""); +} + +sub iset_config_reload_cb +{ + my ($data,$config_file) = ($_[0], $_[1]); + return weechat::config_reload($config_file) +} + +sub iset_config_read +{ + return weechat::config_read($iset_config_file) if ($iset_config_file ne ""); +} + +sub iset_config_write +{ + return weechat::config_write($iset_config_file) if ($iset_config_file ne ""); +} + +sub full_refresh_cb +{ + iset_full_refresh(); + return weechat::WEECHAT_RC_OK; +} + +sub bar_refresh +{ + iset_get_help(1); + weechat::bar_item_update("isetbar_help") if (weechat::config_boolean($options_iset{"show_help_bar"}) == 1); + return weechat::WEECHAT_RC_OK; +} + +sub toggle_help_cb +{ + my $value = weechat::config_boolean($options_iset{"show_help_bar"}); + iset_show_bar($value); + return weechat::WEECHAT_RC_OK; +} + +# -----------------------------------[ main ]----------------------------------------- + +weechat::register($PRGNAME, $AUTHOR, $VERSION, $LICENSE, + $DESCR, "iset_end", ""); + +$wee_version_number = weechat::info_get("version_number", "") || 0; + +iset_config_init(); +iset_config_read(); + +weechat::hook_command($PRGNAME, "Interactive set", "d || f || s

|| [=][=]", + "d : show only changed options\n". + "f file : show options for a file\n". + "s section: show options for a section\n". + "text : show options with 'text' in name\n". + weechat::config_string($options_iset{"value_search_char"})."text : show options with 'text' in value\n". + weechat::config_string($options_iset{"value_search_char"}).weechat::config_string($options_iset{"value_search_char"})."text : show options with exact 'text' in value\n\n". + "Keys for iset buffer:\n". + "f11,f12 : move iset content left/right\n". + "up,down : move one option up/down\n". + "pgup,pdwn : move one page up/down\n". + "home,end : move to first/last option\n". + "ctrl+'L' : refresh options and screen\n". + "alt+space : toggle boolean on/off\n". + "alt+'+' : increase value (for integer or color)\n". + "alt+'-' : decrease value (for integer or color)\n". + "alt+'i',alt+'r': reset value of option\n". + "alt+'i',alt+'u': unset option\n". + "alt+enter : set new value for option (edit it with command line)\n". + "text,enter : set a new filter using command line (use '*' to see all options)\n". + "alt+'v' : toggle help bar on/off\n". + "alt+'p' : toggle option \"show_plugin_description\" on/off\n". + "q : as input in iset buffer to close it\n". + "\n". + "Mouse actions:\n". + "wheel up/down : move cursor up/down\n". + "left button : select an option from list\n". + "right button : toggle boolean (on/off) or set a new value for option (edit it with command line)\n". + "right button + drag left/right: increase/decrease value (for integer or color)\n". + "\n". + "Examples:\n". + " show changed options in 'aspell' plugin\n". + " /iset d aspell\n". + " show options for file 'irc'\n". + " /iset f irc\n". + " show options for section 'look'\n". + " /iset s look\n". + " show all options with text 'nicklist' in name\n". + " /iset nicklist\n". + " show all values which contain 'red'. ('" . weechat::config_string($options_iset{"value_search_char"}) . "' is a trigger char).\n". + " /iset ". weechat::config_string($options_iset{"value_search_char"}) ."red\n". + " show all values which hit 'off'. ('" . weechat::config_string($options_iset{"value_search_char"}) . weechat::config_string($options_iset{"value_search_char"}) . "' is a trigger char).\n". + " /iset ". weechat::config_string($options_iset{"value_search_char"}) . weechat::config_string($options_iset{"value_search_char"}) ."off\n". + " show options for file 'weechat' which contains value 'off'\n". + " /iset f weechat ".weechat::config_string($options_iset{"value_search_char"})."off\n". + "", + "", "iset_cmd_cb", ""); +weechat::hook_signal("upgrade_ended", "iset_upgrade_ended", ""); +weechat::hook_signal("window_scrolled", "iset_signal_window_scrolled_cb", ""); +weechat::hook_signal("buffer_switch", "iset_signal_buffer_switch_cb",""); +weechat::bar_item_new("isetbar_help", "iset_item_cb", ""); +if ($wee_version_number >= 0x02090000) +{ + weechat::bar_new("isetbar", "on", "0", "window", "", "top", "horizontal", + "vertical", "3", "3", "default", "cyan", "default", "default", "1", + "isetbar_help"); +} +else +{ + weechat::bar_new("isetbar", "on", "0", "window", "", "top", "horizontal", + "vertical", "3", "3", "default", "cyan", "default", "1", + "isetbar_help"); +} +weechat::hook_modifier("bar_condition_isetbar", "iset_check_condition_isetbar_cb", ""); +weechat::hook_config("*", "iset_config_cb", ""); +$iset_buffer = weechat::buffer_search($LANG, $PRGNAME); +iset_init() if ($iset_buffer ne ""); + +if ($wee_version_number >= 0x00030600) +{ + weechat::hook_focus("chat", "hook_focus_iset_cb", ""); + weechat::hook_hsignal($PRGNAME."_mouse", "iset_hsignal_mouse_cb", ""); + weechat::key_bind("mouse", \%mouse_keys); +} diff --git a/.config/weechat/python/autoload/aesthetic.py b/.config/weechat/python/autoload/aesthetic.py deleted file mode 120000 index 9c60e1b2..00000000 --- a/.config/weechat/python/autoload/aesthetic.py +++ /dev/null @@ -1 +0,0 @@ -../aesthetic.py \ No newline at end of file diff --git a/.config/weechat/python/autoload/aesthetic.py b/.config/weechat/python/autoload/aesthetic.py new file mode 100644 index 00000000..988071d6 --- /dev/null +++ b/.config/weechat/python/autoload/aesthetic.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# +# Script Name: aesthetic.py +# Script Author: Wojciech Siewierski +# Script License: GPL3 +# Contact: vifon @ irc.freenode.net + +SCRIPT_NAME = 'aesthetic' +SCRIPT_AUTHOR = 'Wojciech Siewierski' +SCRIPT_VERSION = '1.0.6' +SCRIPT_LICENSE = 'GPL3' +SCRIPT_DESC = 'Make messages more A E S T H E T I C A L L Y pleasing.' + +import_ok = True + +try: + import weechat +except ImportError: + print('This script must be run under WeeChat') + print('You can obtain a copy of WeeChat, for free, at https://weechat.org') + import_ok = False + +weechat_version = 0 + +import shlex +import sys + +def aesthetic_(args): + for arg in args: + try: + arg = arg.decode('utf8') + except AttributeError: + pass + yield " ".join(arg.upper()) + for n, char in enumerate(arg[1:]): + yield " ".join(" "*(n+1)).join(char.upper()*2) + +def aesthetic(args): + if sys.version_info < (3,): + return (x.encode('utf8') for x in aesthetic_(args)) + else: + return aesthetic_(args) + +def aesthetic_cb(data, buffer, args): + for x in aesthetic(shlex.split(args)): + weechat.command(buffer, x) + return weechat.WEECHAT_RC_OK + +if __name__ == "__main__" and import_ok: + if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): + weechat_version = weechat.info_get("version_number", "") or 0 + weechat.hook_command( + "aesthetic", + """Format a message like this: + +E X A M P L E +X X +A A +M M +P P +L L +E E + +Each argument is formatted separately, use sh-like quotes for grouping. For example '/aesthetic foo bar' will send two such blocks while '/aesthetic "foo bar"' would send one larger one. + +Use with care to not cause undesirable message spam.""", + "message", "", + "", + "aesthetic_cb", "" + ) diff --git a/.config/weechat/python/autoload/anotify.py b/.config/weechat/python/autoload/anotify.py deleted file mode 120000 index 1517fefe..00000000 --- a/.config/weechat/python/autoload/anotify.py +++ /dev/null @@ -1 +0,0 @@ -../anotify.py \ No newline at end of file diff --git a/.config/weechat/python/autoload/anotify.py b/.config/weechat/python/autoload/anotify.py new file mode 100644 index 00000000..0a047b42 --- /dev/null +++ b/.config/weechat/python/autoload/anotify.py @@ -0,0 +1,476 @@ +# -*- coding: utf-8 -*- +# +# anotify.py +# Copyright (c) 2012 magnific0 +# +# based on: +# growl.py +# Copyright (c) 2011 Sorin Ionescu +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +SCRIPT_NAME = 'anotify' +SCRIPT_AUTHOR = 'magnific0' +SCRIPT_VERSION = '1.0.2' +SCRIPT_LICENSE = 'MIT' +SCRIPT_DESC = 'Sends libnotify notifications upon events.' + + +# Changelog +# 2014-05-10: v1.0.1 Change hook_print callback argument type of +# displayed/highlight (WeeChat >= 1.0) +# 2012-09-20: v1.0.0 Forked from original and adapted for libnotify. + +# ----------------------------------------------------------------------------- +# Settings +# ----------------------------------------------------------------------------- +SETTINGS = { + 'show_public_message': 'off', + 'show_private_message': 'on', + 'show_public_action_message': 'off', + 'show_private_action_message': 'on', + 'show_notice_message': 'off', + 'show_invite_message': 'on', + 'show_highlighted_message': 'on', + 'show_server': 'on', + 'show_channel_topic': 'on', + 'show_dcc': 'on', + 'show_upgrade_ended': 'on', + 'sticky': 'off', + 'sticky_away': 'on', + 'icon': '/usr/share/pixmaps/weechat.xpm', +} + + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +try: + import re + import weechat + import gi + import notify2 + import subprocess + IMPORT_OK = True +except ImportError as error: + IMPORT_OK = False + if str(error).find('weechat') != -1: + print('This script must be run under WeeChat.') + print('Get WeeChat at http://www.weechat.org.') + else: + weechat.prnt('', 'anotify: {0}'.format(error)) + +# ----------------------------------------------------------------------------- +# Globals +# ----------------------------------------------------------------------------- +TAGGED_MESSAGES = { + 'public message or action': set(['irc_privmsg', 'notify_message']), + 'private message or action': set(['irc_privmsg', 'notify_private']), + 'notice message': set(['irc_notice', 'notify_private']), + 'invite message': set(['irc_invite', 'notify_highlight']), + 'channel topic': set(['irc_topic', ]), + #'away status': set(['away_info', ]), +} + + +UNTAGGED_MESSAGES = { + 'away status': + re.compile(r'^You ((\w+).){2,3}marked as being away', re.UNICODE), + 'dcc chat request': + re.compile(r'^xfer: incoming chat request from (\w+)', re.UNICODE), + 'dcc chat closed': + re.compile(r'^xfer: chat closed with (\w+)', re.UNICODE), + 'dcc get request': + re.compile( + r'^xfer: incoming file from (\w+) [^:]+: ((?:,\w|[^,])+),', + re.UNICODE), + 'dcc get completed': + re.compile(r'^xfer: file ([^\s]+) received from \w+: OK', re.UNICODE), + 'dcc get failed': + re.compile( + r'^xfer: file ([^\s]+) received from \w+: FAILED', + re.UNICODE), + 'dcc send completed': + re.compile(r'^xfer: file ([^\s]+) sent to \w+: OK', re.UNICODE), + 'dcc send failed': + re.compile(r'^xfer: file ([^\s]+) sent to \w+: FAILED', re.UNICODE), +} + + +DISPATCH_TABLE = { + 'away status': 'set_away_status', + 'public message or action': 'notify_public_message_or_action', + 'private message or action': 'notify_private_message_or_action', + 'notice message': 'notify_notice_message', + 'invite message': 'notify_invite_message', + 'channel topic': 'notify_channel_topic', + 'dcc chat request': 'notify_dcc_chat_request', + 'dcc chat closed': 'notify_dcc_chat_closed', + 'dcc get request': 'notify_dcc_get_request', + 'dcc get completed': 'notify_dcc_get_completed', + 'dcc get failed': 'notify_dcc_get_failed', + 'dcc send completed': 'notify_dcc_send_completed', + 'dcc send failed': 'notify_dcc_send_failed', +} + + +STATE = { + 'icon': None, + 'is_away': False +} + + +# ----------------------------------------------------------------------------- +# Notifiers +# ----------------------------------------------------------------------------- +def cb_irc_server_connected(data, signal, signal_data): + '''Notify when connected to IRC server.''' + if weechat.config_get_plugin('show_server') == 'on': + a_notify( + 'Server', + 'Server Connected', + 'Connected to network {0}.'.format(signal_data)) + return weechat.WEECHAT_RC_OK + + +def cb_irc_server_disconnected(data, signal, signal_data): + '''Notify when disconnected to IRC server.''' + if weechat.config_get_plugin('show_server') == 'on': + a_notify( + 'Server', + 'Server Disconnected', + 'Disconnected from network {0}.'.format(signal_data)) + return weechat.WEECHAT_RC_OK + + +def cb_notify_upgrade_ended(data, signal, signal_data): + '''Notify on end of WeeChat upgrade.''' + if weechat.config_get_plugin('show_upgrade_ended') == 'on': + a_notify( + 'WeeChat', + 'WeeChat Upgraded', + 'WeeChat has been upgraded.') + return weechat.WEECHAT_RC_OK + + +def notify_highlighted_message(prefix, message): + '''Notify on highlighted message.''' + if weechat.config_get_plugin("show_highlighted_message") == "on": + a_notify( + 'Highlight', + 'Highlighted Message', + "{0}: {1}".format(prefix, message), + priority=notify2.URGENCY_CRITICAL) + + +def notify_public_message_or_action(prefix, message, highlighted): + '''Notify on public message or action.''' + if prefix == ' *': + regex = re.compile(r'^(\w+) (.+)$', re.UNICODE) + match = regex.match(message) + if match: + prefix = match.group(1) + message = match.group(2) + notify_public_action_message(prefix, message, highlighted) + else: + if highlighted: + notify_highlighted_message(prefix, message) + elif weechat.config_get_plugin("show_public_message") == "on": + a_notify( + 'Public', + 'Public Message', + '{0}: {1}'.format(prefix, message)) + + +def notify_private_message_or_action(prefix, message, highlighted): + '''Notify on private message or action.''' + regex = re.compile(r'^CTCP_MESSAGE.+?ACTION (.+)$', re.UNICODE) + match = regex.match(message) + if match: + notify_private_action_message(prefix, match.group(1), highlighted) + else: + if prefix == ' *': + regex = re.compile(r'^(\w+) (.+)$', re.UNICODE) + match = regex.match(message) + if match: + prefix = match.group(1) + message = match.group(2) + notify_private_action_message(prefix, message, highlighted) + else: + if highlighted: + notify_highlighted_message(prefix, message) + elif weechat.config_get_plugin("show_private_message") == "on": + a_notify( + 'Private', + 'Private Message', + '{0}: {1}'.format(prefix, message)) + + +def notify_public_action_message(prefix, message, highlighted): + '''Notify on public action message.''' + if highlighted: + notify_highlighted_message(prefix, message) + elif weechat.config_get_plugin("show_public_action_message") == "on": + a_notify( + 'Action', + 'Public Action Message', + '{0}: {1}'.format(prefix, message), + priority=notify2.URGENCY_NORMAL) + + +def notify_private_action_message(prefix, message, highlighted): + '''Notify on private action message.''' + if highlighted: + notify_highlighted_message(prefix, message) + elif weechat.config_get_plugin("show_private_action_message") == "on": + a_notify( + 'Action', + 'Private Action Message', + '{0}: {1}'.format(prefix, message), + priority=notify2.URGENCY_NORMAL) + + +def notify_notice_message(prefix, message, highlighted): + '''Notify on notice message.''' + regex = re.compile(r'^([^\s]*) [^:]*: (.+)$', re.UNICODE) + match = regex.match(message) + if match: + prefix = match.group(1) + message = match.group(2) + if highlighted: + notify_highlighted_message(prefix, message) + elif weechat.config_get_plugin("show_notice_message") == "on": + a_notify( + 'Notice', + 'Notice Message', + '{0}: {1}'.format(prefix, message)) + + +def notify_invite_message(prefix, message, highlighted): + '''Notify on channel invitation message.''' + if weechat.config_get_plugin("show_invite_message") == "on": + regex = re.compile( + r'^You have been invited to ([^\s]+) by ([^\s]+)$', re.UNICODE) + match = regex.match(message) + if match: + channel = match.group(1) + nick = match.group(2) + a_notify( + 'Invite', + 'Channel Invitation', + '{0} has invited you to join {1}.'.format(nick, channel)) + + +def notify_channel_topic(prefix, message, highlighted): + '''Notify on channel topic change.''' + if weechat.config_get_plugin("show_channel_topic") == "on": + regex = re.compile( + r'^\w+ has (?:changed|unset) topic for ([^\s]+)' + + '(?:(?: from "(?:(?:"\w|[^"])+)")? to "((?:"\w|[^"])+)")?', + re.UNICODE) + match = regex.match(message) + if match: + channel = match.group(1) + topic = match.group(2) or '' + a_notify( + 'Channel', + 'Channel Topic', + "{0}: {1}".format(channel, topic)) + + +def notify_dcc_chat_request(match): + '''Notify on DCC chat request.''' + if weechat.config_get_plugin("show_dcc") == "on": + nick = match.group(1) + a_notify( + 'DCC', + 'Direct Chat Request', + '{0} wants to chat directly.'.format(nick)) + + +def notify_dcc_chat_closed(match): + '''Notify on DCC chat termination.''' + if weechat.config_get_plugin("show_dcc") == "on": + nick = match.group(1) + a_notify( + 'DCC', + 'Direct Chat Ended', + 'Direct chat with {0} has ended.'.format(nick)) + + +def notify_dcc_get_request(match): + 'Notify on DCC get request.' + if weechat.config_get_plugin("show_dcc") == "on": + nick = match.group(1) + file_name = match.group(2) + a_notify( + 'DCC', + 'File Transfer Request', + '{0} wants to send you {1}.'.format(nick, file_name)) + + +def notify_dcc_get_completed(match): + 'Notify on DCC get completion.' + if weechat.config_get_plugin("show_dcc") == "on": + file_name = match.group(1) + a_notify('DCC', 'Download Complete', file_name) + + +def notify_dcc_get_failed(match): + 'Notify on DCC get failure.' + if weechat.config_get_plugin("show_dcc") == "on": + file_name = match.group(1) + a_notify('DCC', 'Download Failed', file_name) + + +def notify_dcc_send_completed(match): + 'Notify on DCC send completion.' + if weechat.config_get_plugin("show_dcc") == "on": + file_name = match.group(1) + a_notify('DCC', 'Upload Complete', file_name) + + +def notify_dcc_send_failed(match): + 'Notify on DCC send failure.' + if weechat.config_get_plugin("show_dcc") == "on": + file_name = match.group(1) + a_notify('DCC', 'Upload Failed', file_name) + + +# ----------------------------------------------------------------------------- +# Utility +# ----------------------------------------------------------------------------- +def set_away_status(match): + status = match.group(1) + if status == 'been ': + STATE['is_away'] = True + if status == 'longer ': + STATE['is_away'] = False + + +def cb_process_message( + data, + wbuffer, + date, + tags, + displayed, + highlight, + prefix, + message +): + '''Delegates incoming messages to appropriate handlers.''' + tags = set(tags.split(',')) + functions = globals() + is_public_message = tags.issuperset( + TAGGED_MESSAGES['public message or action']) + buffer_name = weechat.buffer_get_string(wbuffer, 'name') + dcc_buffer_regex = re.compile(r'^irc_dcc\.', re.UNICODE) + dcc_buffer_match = dcc_buffer_regex.match(buffer_name) + highlighted = False + if int(highlight): + highlighted = True + # Private DCC message identifies itself as public. + if is_public_message and dcc_buffer_match: + notify_private_message_or_action(prefix, message, highlighted) + return weechat.WEECHAT_RC_OK + # Pass identified, untagged message to its designated function. + for key, value in UNTAGGED_MESSAGES.items(): + match = value.match(message) + if match: + functions[DISPATCH_TABLE[key]](match) + return weechat.WEECHAT_RC_OK + # Pass identified, tagged message to its designated function. + for key, value in TAGGED_MESSAGES.items(): + if tags.issuperset(value): + functions[DISPATCH_TABLE[key]](prefix, message, highlighted) + return weechat.WEECHAT_RC_OK + return weechat.WEECHAT_RC_OK + + +def a_notify(notification, title, description, priority=notify2.URGENCY_LOW): + '''Returns whether notifications should be sticky.''' + is_away = STATE['is_away'] + icon = STATE['icon'] + time_out = 5000 + if weechat.config_get_plugin('sticky') == 'on': + time_out = 0 + if weechat.config_get_plugin('sticky_away') == 'on' and is_away: + time_out = 0 + try: + # notify2.init("wee-notifier") + # wn = notify2.Notification(title, description, icon) + # wn.set_urgency(priority) + # wn.set_timeout(time_out) + # wn.show() + subprocess.Popen(["notify-send", "-a", " WeeChat", title, description]) + if title != "Server Connected" and title != "Server Disconnected": + subprocess.Popen(["canberra-gtk-play", "-i", "message-new-instant", "-V", "15"]) + except Exception as error: + weechat.prnt('', 'anotify: {0}'.format(error)) + + +# ----------------------------------------------------------------------------- +# Main +# ----------------------------------------------------------------------------- +def main(): + '''Sets up WeeChat notifications.''' + # Initialize options. + for option, value in SETTINGS.items(): + if not weechat.config_is_set_plugin(option): + weechat.config_set_plugin(option, value) + # Initialize. + name = "WeeChat" + icon = "/usr/share/pixmaps/weechat.xpm" + notifications = [ + 'Public', + 'Private', + 'Action', + 'Notice', + 'Invite', + 'Highlight', + 'Server', + 'Channel', + 'DCC', + 'WeeChat' + ] + STATE['icon'] = icon + # Register hooks. + weechat.hook_signal( + 'irc_server_connected', + 'cb_irc_server_connected', + '') + weechat.hook_signal( + 'irc_server_disconnected', + 'cb_irc_server_disconnected', + '') + weechat.hook_signal('upgrade_ended', 'cb_upgrade_ended', '') + weechat.hook_print('', '', '', 1, 'cb_process_message', '') + + +if __name__ == '__main__' and IMPORT_OK and weechat.register( + SCRIPT_NAME, + SCRIPT_AUTHOR, + SCRIPT_VERSION, + SCRIPT_LICENSE, + SCRIPT_DESC, + '', + '' +): + main() diff --git a/.config/weechat/python/autoload/autosort.py b/.config/weechat/python/autoload/autosort.py deleted file mode 120000 index 1850897f..00000000 --- a/.config/weechat/python/autoload/autosort.py +++ /dev/null @@ -1 +0,0 @@ -../autosort.py \ No newline at end of file diff --git a/.config/weechat/python/autoload/autosort.py b/.config/weechat/python/autoload/autosort.py new file mode 100644 index 00000000..4312cda7 --- /dev/null +++ b/.config/weechat/python/autoload/autosort.py @@ -0,0 +1,1075 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013-2017 Maarten de Vries +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# +# Autosort automatically keeps your buffers sorted and grouped by server. +# You can define your own sorting rules. See /help autosort for more details. +# +# https://github.com/de-vri-es/weechat-autosort +# + +# +# Changelog: +# 3.9: +# * Remove `buffers.pl` from recommended settings. +# 3,8: +# * Fix relative sorting on script name in default rules. +# * Document a useful property of stable sort algorithms. +# 3.7: +# * Make default rules work with bitlbee, matrix and slack. +# 3.6: +# * Add more documentation on provided info hooks. +# 3.5: +# * Add ${info:autosort_escape,...} to escape arguments for other info hooks. +# 3.4: +# * Fix rate-limit of sorting to prevent high CPU load and lock-ups. +# * Fix bug in parsing empty arguments for info hooks. +# * Add debug_log option to aid with debugging. +# * Correct a few typos. +# 3.3: +# * Fix the /autosort debug command for unicode. +# * Update the default rules to work better with Slack. +# 3.2: +# * Fix python3 compatiblity. +# 3.1: +# * Use colors to format the help text. +# 3.0: +# * Switch to evaluated expressions for sorting. +# * Add `/autosort debug` command. +# * Add ${info:autosort_replace,from,to,text} to replace substrings in sort rules. +# * Add ${info:autosort_order,value,first,second,third} to ease writing sort rules. +# * Make tab completion context aware. +# 2.8: +# * Fix compatibility with python 3 regarding unicode handling. +# 2.7: +# * Fix sorting of buffers with spaces in their name. +# 2.6: +# * Ignore case in rules when doing case insensitive sorting. +# 2.5: +# * Fix handling unicode buffer names. +# * Add hint to set irc.look.server_buffer to independent and buffers.look.indenting to on. +# 2.4: +# * Make script python3 compatible. +# 2.3: +# * Fix sorting items without score last (regressed in 2.2). +# 2.2: +# * Add configuration option for signals that trigger a sort. +# * Add command to manually trigger a sort (/autosort sort). +# * Add replacement patterns to apply before sorting. +# 2.1: +# * Fix some minor style issues. +# 2.0: +# * Allow for custom sort rules. +# + + +import json +import math +import re +import sys +import time +import weechat + +SCRIPT_NAME = 'autosort' +SCRIPT_AUTHOR = 'Maarten de Vries ' +SCRIPT_VERSION = '3.9' +SCRIPT_LICENSE = 'GPL3' +SCRIPT_DESC = 'Flexible automatic (or manual) buffer sorting based on eval expressions.' + + +config = None +hooks = [] +signal_delay_timer = None +sort_limit_timer = None +sort_queued = False + + +# Make sure that unicode, bytes and str are always available in python2 and 3. +# For python 2, str == bytes +# For python 3, str == unicode +if sys.version_info[0] >= 3: + unicode = str + +def ensure_str(input): + ''' + Make sure the given type if the correct string type for the current python version. + That means bytes for python2 and unicode for python3. + ''' + if not isinstance(input, str): + if isinstance(input, bytes): + return input.encode('utf-8') + if isinstance(input, unicode): + return input.decode('utf-8') + return input + + +if hasattr(time, 'perf_counter'): + perf_counter = time.perf_counter +else: + perf_counter = time.clock + +def casefold(string): + if hasattr(string, 'casefold'): return string.casefold() + # Fall back to lowercasing for python2. + return string.lower() + +def list_swap(values, a, b): + values[a], values[b] = values[b], values[a] + +def list_move(values, old_index, new_index): + values.insert(new_index, values.pop(old_index)) + +def list_find(collection, value): + for i, elem in enumerate(collection): + if elem == value: return i + return None + +class HumanReadableError(Exception): + pass + +def parse_int(arg, arg_name = 'argument'): + ''' Parse an integer and provide a more human readable error. ''' + arg = arg.strip() + try: + return int(arg) + except ValueError: + raise HumanReadableError('Invalid {0}: expected integer, got "{1}".'.format(arg_name, arg)) + +def decode_rules(blob): + parsed = json.loads(blob) + if not isinstance(parsed, list): + log('Malformed rules, expected a JSON encoded list of strings, but got a {0}. No rules have been loaded. Please fix the setting manually.'.format(type(parsed))) + return [] + + for i, entry in enumerate(parsed): + if not isinstance(entry, (str, unicode)): + log('Rule #{0} is not a string but a {1}. No rules have been loaded. Please fix the setting manually.'.format(i, type(entry))) + return [] + + return parsed + +def decode_helpers(blob): + parsed = json.loads(blob) + if not isinstance(parsed, dict): + log('Malformed helpers, expected a JSON encoded dictionary but got a {0}. No helpers have been loaded. Please fix the setting manually.'.format(type(parsed))) + return {} + + for key, value in parsed.items(): + if not isinstance(value, (str, unicode)): + log('Helper "{0}" is not a string but a {1}. No helpers have been loaded. Please fix setting manually.'.format(key, type(value))) + return {} + return parsed + +class Config: + ''' The autosort configuration. ''' + + default_rules = json.dumps([ + '${core_first}', + '${info:autosort_order,${info:autosort_escape,${script_or_plugin}},core,*,irc,bitlbee,matrix,slack}', + '${script_or_plugin}', + '${irc_raw_first}', + '${server}', + '${info:autosort_order,${type},server,*,channel,private}', + '${hashless_name}', + '${buffer.full_name}', + ]) + + default_helpers = json.dumps({ + 'core_first': '${if:${buffer.full_name}!=core.weechat}', + 'irc_raw_first': '${if:${buffer.full_name}!=irc.irc_raw}', + 'irc_raw_last': '${if:${buffer.full_name}==irc.irc_raw}', + 'hashless_name': '${info:autosort_replace,#,,${info:autosort_escape,${buffer.name}}}', + 'script_or_plugin': '${if:${script_name}?${script_name}:${plugin}}', + }) + + default_signal_delay = 5 + default_sort_limit = 100 + + default_signals = 'buffer_opened buffer_merged buffer_unmerged buffer_renamed' + + def __init__(self, filename): + ''' Initialize the configuration. ''' + + self.filename = filename + self.config_file = weechat.config_new(self.filename, '', '') + self.sorting_section = None + self.v3_section = None + + self.case_sensitive = False + self.rules = [] + self.helpers = {} + self.signals = [] + self.signal_delay = Config.default_signal_delay, + self.sort_limit = Config.default_sort_limit, + self.sort_on_config = True + self.debug_log = False + + self.__case_sensitive = None + self.__rules = None + self.__helpers = None + self.__signals = None + self.__signal_delay = None + self.__sort_limit = None + self.__sort_on_config = None + self.__debug_log = None + + if not self.config_file: + log('Failed to initialize configuration file "{0}".'.format(self.filename)) + return + + self.sorting_section = weechat.config_new_section(self.config_file, 'sorting', False, False, '', '', '', '', '', '', '', '', '', '') + self.v3_section = weechat.config_new_section(self.config_file, 'v3', False, False, '', '', '', '', '', '', '', '', '', '') + + if not self.sorting_section: + log('Failed to initialize section "sorting" of configuration file.') + weechat.config_free(self.config_file) + return + + self.__case_sensitive = weechat.config_new_option( + self.config_file, self.sorting_section, + 'case_sensitive', 'boolean', + 'If this option is on, sorting is case sensitive.', + '', 0, 0, 'off', 'off', 0, + '', '', '', '', '', '' + ) + + weechat.config_new_option( + self.config_file, self.sorting_section, + 'rules', 'string', + 'Sort rules used by autosort v2.x and below. Not used by autosort anymore.', + '', 0, 0, '', '', 0, + '', '', '', '', '', '' + ) + + weechat.config_new_option( + self.config_file, self.sorting_section, + 'replacements', 'string', + 'Replacement patterns used by autosort v2.x and below. Not used by autosort anymore.', + '', 0, 0, '', '', 0, + '', '', '', '', '', '' + ) + + self.__rules = weechat.config_new_option( + self.config_file, self.v3_section, + 'rules', 'string', + 'An ordered list of sorting rules encoded as JSON. See /help autosort for commands to manipulate these rules.', + '', 0, 0, Config.default_rules, Config.default_rules, 0, + '', '', '', '', '', '' + ) + + self.__helpers = weechat.config_new_option( + self.config_file, self.v3_section, + 'helpers', 'string', + 'A dictionary helper variables to use in the sorting rules, encoded as JSON. See /help autosort for commands to manipulate these helpers.', + '', 0, 0, Config.default_helpers, Config.default_helpers, 0, + '', '', '', '', '', '' + ) + + self.__signals = weechat.config_new_option( + self.config_file, self.sorting_section, + 'signals', 'string', + 'A space separated list of signals that will cause autosort to resort your buffer list.', + '', 0, 0, Config.default_signals, Config.default_signals, 0, + '', '', '', '', '', '' + ) + + self.__signal_delay = weechat.config_new_option( + self.config_file, self.sorting_section, + 'signal_delay', 'integer', + 'Delay in milliseconds to wait after a signal before sorting the buffer list. This prevents triggering many times if multiple signals arrive in a short time. It can also be needed to wait for buffer localvars to be available.', + '', 0, 1000, str(Config.default_signal_delay), str(Config.default_signal_delay), 0, + '', '', '', '', '', '' + ) + + self.__sort_limit = weechat.config_new_option( + self.config_file, self.sorting_section, + 'sort_limit', 'integer', + 'Minimum delay in milliseconds to wait after sorting before signals can trigger a sort again. This is effectively a rate limit on sorting. Keeping signal_delay low while setting this higher can reduce excessive sorting without a long initial delay.', + '', 0, 1000, str(Config.default_sort_limit), str(Config.default_sort_limit), 0, + '', '', '', '', '', '' + ) + + self.__sort_on_config = weechat.config_new_option( + self.config_file, self.sorting_section, + 'sort_on_config_change', 'boolean', + 'Decides if the buffer list should be sorted when autosort configuration changes.', + '', 0, 0, 'on', 'on', 0, + '', '', '', '', '', '' + ) + + self.__debug_log = weechat.config_new_option( + self.config_file, self.sorting_section, + 'debug_log', 'boolean', + 'If enabled, print more debug messages. Not recommended for normal usage.', + '', 0, 0, 'off', 'off', 0, + '', '', '', '', '', '' + ) + + if weechat.config_read(self.config_file) != weechat.WEECHAT_RC_OK: + log('Failed to load configuration file.') + + if weechat.config_write(self.config_file) != weechat.WEECHAT_RC_OK: + log('Failed to write configuration file.') + + self.reload() + + def reload(self): + ''' Load configuration variables. ''' + + self.case_sensitive = weechat.config_boolean(self.__case_sensitive) + + rules_blob = weechat.config_string(self.__rules) + helpers_blob = weechat.config_string(self.__helpers) + signals_blob = weechat.config_string(self.__signals) + + self.rules = decode_rules(rules_blob) + self.helpers = decode_helpers(helpers_blob) + self.signals = signals_blob.split() + self.signal_delay = weechat.config_integer(self.__signal_delay) + self.sort_limit = weechat.config_integer(self.__sort_limit) + self.sort_on_config = weechat.config_boolean(self.__sort_on_config) + self.debug_log = weechat.config_boolean(self.__debug_log) + + def save_rules(self, run_callback = True): + ''' Save the current rules to the configuration. ''' + weechat.config_option_set(self.__rules, json.dumps(self.rules), run_callback) + + def save_helpers(self, run_callback = True): + ''' Save the current helpers to the configuration. ''' + weechat.config_option_set(self.__helpers, json.dumps(self.helpers), run_callback) + + +def pad(sequence, length, padding = None): + ''' Pad a list until is has a certain length. ''' + return sequence + [padding] * max(0, (length - len(sequence))) + +def log(message, buffer = 'NULL'): + weechat.prnt(buffer, 'autosort: {0}'.format(message)) + +def debug(message, buffer = 'NULL'): + if config.debug_log: + weechat.prnt(buffer, 'autosort: debug: {0}'.format(message)) + +def get_buffers(): + ''' Get a list of all the buffers in weechat. ''' + hdata = weechat.hdata_get('buffer') + buffer = weechat.hdata_get_list(hdata, "gui_buffers"); + + result = [] + while buffer: + number = weechat.hdata_integer(hdata, buffer, 'number') + result.append((number, buffer)) + buffer = weechat.hdata_pointer(hdata, buffer, 'next_buffer') + return hdata, result + +class MergedBuffers(list): + """ A list of merged buffers, possibly of size 1. """ + def __init__(self, number): + super(MergedBuffers, self).__init__() + self.number = number + +def merge_buffer_list(buffers): + ''' + Group merged buffers together. + The output is a list of MergedBuffers. + ''' + if not buffers: return [] + result = {} + for number, buffer in buffers: + if number not in result: result[number] = MergedBuffers(number) + result[number].append(buffer) + return result.values() + +def sort_buffers(hdata, buffers, rules, helpers, case_sensitive): + for merged in buffers: + for buffer in merged: + name = weechat.hdata_string(hdata, buffer, 'name') + + return sorted(buffers, key=merged_sort_key(rules, helpers, case_sensitive)) + +def buffer_sort_key(rules, helpers, case_sensitive): + ''' Create a sort key function for a list of lists of merged buffers. ''' + def key(buffer): + extra_vars = {} + for helper_name, helper in sorted(helpers.items()): + expanded = weechat.string_eval_expression(helper, {"buffer": buffer}, {}, {}) + extra_vars[helper_name] = expanded if case_sensitive else casefold(expanded) + result = [] + for rule in rules: + expanded = weechat.string_eval_expression(rule, {"buffer": buffer}, extra_vars, {}) + result.append(expanded if case_sensitive else casefold(expanded)) + return result + + return key + +def merged_sort_key(rules, helpers, case_sensitive): + buffer_key = buffer_sort_key(rules, helpers, case_sensitive) + def key(merged): + best = None + for buffer in merged: + this = buffer_key(buffer) + if best is None or this < best: best = this + return best + return key + +def apply_buffer_order(buffers): + ''' Sort the buffers in weechat according to the given order. ''' + for i, buffer in enumerate(buffers): + weechat.buffer_set(buffer[0], "number", str(i + 1)) + +def split_args(args, expected, optional = 0): + ''' Split an argument string in the desired number of arguments. ''' + split = args.split(' ', expected - 1) + if (len(split) < expected): + raise HumanReadableError('Expected at least {0} arguments, got {1}.'.format(expected, len(split))) + return split[:-1] + pad(split[-1].split(' ', optional), optional + 1, '') + +def do_sort(verbose = False): + start = perf_counter() + + hdata, buffers = get_buffers() + buffers = merge_buffer_list(buffers) + buffers = sort_buffers(hdata, buffers, config.rules, config.helpers, config.case_sensitive) + apply_buffer_order(buffers) + + elapsed = perf_counter() - start + if verbose: + log("Finished sorting buffers in {0:.4f} seconds.".format(elapsed)) + else: + debug("Finished sorting buffers in {0:.4f} seconds.".format(elapsed)) + +def command_sort(buffer, command, args): + ''' Sort the buffers and print a confirmation. ''' + do_sort(True) + return weechat.WEECHAT_RC_OK + +def command_debug(buffer, command, args): + hdata, buffers = get_buffers() + buffers = merge_buffer_list(buffers) + + # Show evaluation results. + log('Individual evaluation results:') + start = perf_counter() + key = buffer_sort_key(config.rules, config.helpers, config.case_sensitive) + results = [] + for merged in buffers: + for buffer in merged: + fullname = weechat.hdata_string(hdata, buffer, 'full_name') + results.append((fullname, key(buffer))) + elapsed = perf_counter() - start + + for fullname, result in results: + fullname = ensure_str(fullname) + result = [ensure_str(x) for x in result] + log('{0}: {1}'.format(fullname, result)) + log('Computing evaluation results took {0:.4f} seconds.'.format(elapsed)) + + return weechat.WEECHAT_RC_OK + +def command_rule_list(buffer, command, args): + ''' Show the list of sorting rules. ''' + output = 'Sorting rules:\n' + for i, rule in enumerate(config.rules): + output += ' {0}: {1}\n'.format(i, rule) + if not len(config.rules): + output += ' No sorting rules configured.\n' + log(output ) + + return weechat.WEECHAT_RC_OK + + +def command_rule_add(buffer, command, args): + ''' Add a rule to the rule list. ''' + config.rules.append(args) + config.save_rules() + command_rule_list(buffer, command, '') + + return weechat.WEECHAT_RC_OK + + +def command_rule_insert(buffer, command, args): + ''' Insert a rule at the desired position in the rule list. ''' + index, rule = split_args(args, 2) + index = parse_int(index, 'index') + + config.rules.insert(index, rule) + config.save_rules() + command_rule_list(buffer, command, '') + return weechat.WEECHAT_RC_OK + + +def command_rule_update(buffer, command, args): + ''' Update a rule in the rule list. ''' + index, rule = split_args(args, 2) + index = parse_int(index, 'index') + + config.rules[index] = rule + config.save_rules() + command_rule_list(buffer, command, '') + return weechat.WEECHAT_RC_OK + + +def command_rule_delete(buffer, command, args): + ''' Delete a rule from the rule list. ''' + index = args.strip() + index = parse_int(index, 'index') + + config.rules.pop(index) + config.save_rules() + command_rule_list(buffer, command, '') + return weechat.WEECHAT_RC_OK + + +def command_rule_move(buffer, command, args): + ''' Move a rule to a new position. ''' + index_a, index_b = split_args(args, 2) + index_a = parse_int(index_a, 'index') + index_b = parse_int(index_b, 'index') + + list_move(config.rules, index_a, index_b) + config.save_rules() + command_rule_list(buffer, command, '') + return weechat.WEECHAT_RC_OK + + +def command_rule_swap(buffer, command, args): + ''' Swap two rules. ''' + index_a, index_b = split_args(args, 2) + index_a = parse_int(index_a, 'index') + index_b = parse_int(index_b, 'index') + + list_swap(config.rules, index_a, index_b) + config.save_rules() + command_rule_list(buffer, command, '') + return weechat.WEECHAT_RC_OK + + +def command_helper_list(buffer, command, args): + ''' Show the list of helpers. ''' + output = 'Helper variables:\n' + + width = max(map(lambda x: len(x) if len(x) <= 30 else 0, config.helpers.keys())) + + for name, expression in sorted(config.helpers.items()): + output += ' {0:>{width}}: {1}\n'.format(name, expression, width=width) + if not len(config.helpers): + output += ' No helper variables configured.' + log(output) + + return weechat.WEECHAT_RC_OK + + +def command_helper_set(buffer, command, args): + ''' Add/update a helper to the helper list. ''' + name, expression = split_args(args, 2) + + config.helpers[name] = expression + config.save_helpers() + command_helper_list(buffer, command, '') + + return weechat.WEECHAT_RC_OK + +def command_helper_delete(buffer, command, args): + ''' Delete a helper from the helper list. ''' + name = args.strip() + + del config.helpers[name] + config.save_helpers() + command_helper_list(buffer, command, '') + return weechat.WEECHAT_RC_OK + + +def command_helper_rename(buffer, command, args): + ''' Rename a helper to a new position. ''' + old_name, new_name = split_args(args, 2) + + try: + config.helpers[new_name] = config.helpers[old_name] + del config.helpers[old_name] + except KeyError: + raise HumanReadableError('No such helper: {0}'.format(old_name)) + config.save_helpers() + command_helper_list(buffer, command, '') + return weechat.WEECHAT_RC_OK + + +def command_helper_swap(buffer, command, args): + ''' Swap two helpers. ''' + a, b = split_args(args, 2) + try: + config.helpers[b], config.helpers[a] = config.helpers[a], config.helpers[b] + except KeyError as e: + raise HumanReadableError('No such helper: {0}'.format(e.args[0])) + + config.helpers.swap(index_a, index_b) + config.save_helpers() + command_helper_list(buffer, command, '') + return weechat.WEECHAT_RC_OK + +def call_command(buffer, command, args, subcommands): + ''' Call a subcommand from a dictionary. ''' + subcommand, tail = pad(args.split(' ', 1), 2, '') + subcommand = subcommand.strip() + if (subcommand == ''): + child = subcommands.get(' ') + else: + command = command + [subcommand] + child = subcommands.get(subcommand) + + if isinstance(child, dict): + return call_command(buffer, command, tail, child) + elif callable(child): + return child(buffer, command, tail) + + log('{0}: command not found'.format(' '.join(command))) + return weechat.WEECHAT_RC_ERROR + +def on_signal(data, signal, signal_data): + global signal_delay_timer + global sort_queued + + # If the sort limit timeout is started, we're in the hold-off time after sorting, just queue a sort. + if sort_limit_timer is not None: + if sort_queued: + debug('Signal {0} ignored, sort limit timeout is active and sort is already queued.'.format(signal)) + else: + debug('Signal {0} received but sort limit timeout is active, sort is now queued.'.format(signal)) + sort_queued = True + return weechat.WEECHAT_RC_OK + + # If the signal delay timeout is started, a signal was recently received, so ignore this signal. + if signal_delay_timer is not None: + debug('Signal {0} ignored, signal delay timeout active.'.format(signal)) + return weechat.WEECHAT_RC_OK + + # Otherwise, start the signal delay timeout. + debug('Signal {0} received, starting signal delay timeout of {1} ms.'.format(signal, config.signal_delay)) + weechat.hook_timer(config.signal_delay, 0, 1, "on_signal_delay_timeout", "") + return weechat.WEECHAT_RC_OK + +def on_signal_delay_timeout(pointer, remaining_calls): + """ Called when the signal_delay_timer triggers. """ + global signal_delay_timer + global sort_limit_timer + global sort_queued + + signal_delay_timer = None + + # If the sort limit timeout was started, we're still in the no-sort period, so just queue a sort. + if sort_limit_timer is not None: + debug('Signal delay timeout expired, but sort limit timeout is active, sort is now queued.') + sort_queued = True + return weechat.WEECHAT_RC_OK + + # Time to sort! + debug('Signal delay timeout expired, starting sort.') + do_sort() + + # Start the sort limit timeout if not disabled. + if config.sort_limit > 0: + debug('Starting sort limit timeout of {0} ms.'.format(config.sort_limit)) + sort_limit_timer = weechat.hook_timer(config.sort_limit, 0, 1, "on_sort_limit_timeout", "") + + return weechat.WEECHAT_RC_OK + +def on_sort_limit_timeout(pointer, remainin_calls): + """ Called when de sort_limit_timer triggers. """ + global sort_limit_timer + global sort_queued + + # If no signal was received during the timeout, we're done. + if not sort_queued: + debug('Sort limit timeout expired without receiving a signal.') + sort_limit_timer = None + return weechat.WEECHAT_RC_OK + + # Otherwise it's time to sort. + debug('Signal received during sort limit timeout, starting queued sort.') + do_sort() + sort_queued = False + + # Start the sort limit timeout again if not disabled. + if config.sort_limit > 0: + debug('Starting sort limit timeout of {0} ms.'.format(config.sort_limit)) + sort_limit_timer = weechat.hook_timer(config.sort_limit, 0, 1, "on_sort_limit_timeout", "") + + return weechat.WEECHAT_RC_OK + + +def apply_config(): + # Unhook all signals and hook the new ones. + for hook in hooks: + weechat.unhook(hook) + for signal in config.signals: + hooks.append(weechat.hook_signal(signal, 'on_signal', '')) + + if config.sort_on_config: + debug('Sorting because configuration changed.') + do_sort() + +def on_config_changed(*args, **kwargs): + ''' Called whenever the configuration changes. ''' + config.reload() + apply_config() + + return weechat.WEECHAT_RC_OK + +def parse_arg(args): + if not args: return '', None + + result = '' + escaped = False + for i, c in enumerate(args): + if not escaped: + if c == '\\': + escaped = True + continue + elif c == ',': + return result, args[i+1:] + result += c + escaped = False + return result, None + +def parse_args(args, max = None): + result = [] + i = 0 + while max is None or i < max: + i += 1 + arg, args = parse_arg(args) + if arg is None: break + result.append(arg) + if args is None: break + return result, args + +def on_info_escape(pointer, name, arguments): + result = '' + for c in arguments: + if c == '\\': + result += '\\\\' + elif c == ',': + result += '\\,' + else: + result +=c + return result + +def on_info_replace(pointer, name, arguments): + arguments, rest = parse_args(arguments, 3) + if rest or len(arguments) < 3: + log('usage: ${{info:{0},old,new,text}}'.format(name)) + return '' + old, new, text = arguments + + return text.replace(old, new) + +def on_info_order(pointer, name, arguments): + arguments, rest = parse_args(arguments) + if len(arguments) < 1: + log('usage: ${{info:{0},value,first,second,third,...}}'.format(name)) + return '' + + value = arguments[0] + keys = arguments[1:] + if not keys: return '0' + + # Find the value in the keys (or '*' if we can't find it) + result = list_find(keys, value) + if result is None: result = list_find(keys, '*') + if result is None: result = len(keys) + + # Pad result with leading zero to make sure string sorting works. + width = int(math.log10(len(keys))) + 1 + return '{0:0{1}}'.format(result, width) + + +def on_autosort_command(data, buffer, args): + ''' Called when the autosort command is invoked. ''' + try: + return call_command(buffer, ['/autosort'], args, { + ' ': command_sort, + 'sort': command_sort, + 'debug': command_debug, + + 'rules': { + ' ': command_rule_list, + 'list': command_rule_list, + 'add': command_rule_add, + 'insert': command_rule_insert, + 'update': command_rule_update, + 'delete': command_rule_delete, + 'move': command_rule_move, + 'swap': command_rule_swap, + }, + 'helpers': { + ' ': command_helper_list, + 'list': command_helper_list, + 'set': command_helper_set, + 'delete': command_helper_delete, + 'rename': command_helper_rename, + 'swap': command_helper_swap, + }, + }) + except HumanReadableError as e: + log(e) + return weechat.WEECHAT_RC_ERROR + +def add_completions(completion, words): + for word in words: + weechat.hook_completion_list_add(completion, word, 0, weechat.WEECHAT_LIST_POS_END) + +def autosort_complete_rules(words, completion): + if len(words) == 0: + add_completions(completion, ['add', 'delete', 'insert', 'list', 'move', 'swap', 'update']) + if len(words) == 1 and words[0] in ('delete', 'insert', 'move', 'swap', 'update'): + add_completions(completion, map(str, range(len(config.rules)))) + if len(words) == 2 and words[0] in ('move', 'swap'): + add_completions(completion, map(str, range(len(config.rules)))) + if len(words) == 2 and words[0] in ('update'): + try: + add_completions(completion, [config.rules[int(words[1])]]) + except KeyError: pass + except ValueError: pass + else: + add_completions(completion, ['']) + return weechat.WEECHAT_RC_OK + +def autosort_complete_helpers(words, completion): + if len(words) == 0: + add_completions(completion, ['delete', 'list', 'rename', 'set', 'swap']) + elif len(words) == 1 and words[0] in ('delete', 'rename', 'set', 'swap'): + add_completions(completion, sorted(config.helpers.keys())) + elif len(words) == 2 and words[0] == 'swap': + add_completions(completion, sorted(config.helpers.keys())) + elif len(words) == 2 and words[0] == 'rename': + add_completions(completion, sorted(config.helpers.keys())) + elif len(words) == 2 and words[0] == 'set': + try: + add_completions(completion, [config.helpers[words[1]]]) + except KeyError: pass + return weechat.WEECHAT_RC_OK + +def on_autosort_complete(data, name, buffer, completion): + cmdline = weechat.buffer_get_string(buffer, "input") + cursor = weechat.buffer_get_integer(buffer, "input_pos") + prefix = cmdline[:cursor] + words = prefix.split()[1:] + + # If the current word isn't finished yet, + # ignore it for coming up with completion suggestions. + if prefix[-1] != ' ': words = words[:-1] + + if len(words) == 0: + add_completions(completion, ['debug', 'helpers', 'rules', 'sort']) + elif words[0] == 'rules': + return autosort_complete_rules(words[1:], completion) + elif words[0] == 'helpers': + return autosort_complete_helpers(words[1:], completion) + return weechat.WEECHAT_RC_OK + +command_description = r'''{*white}# General commands{reset} + +{*white}/autosort {brown}sort{reset} +Manually trigger the buffer sorting. + +{*white}/autosort {brown}debug{reset} +Show the evaluation results of the sort rules for each buffer. + + +{*white}# Sorting rule commands{reset} + +{*white}/autosort{brown} rules list{reset} +Print the list of sort rules. + +{*white}/autosort {brown}rules add {cyan}{reset} +Add a new rule at the end of the list. + +{*white}/autosort {brown}rules insert {cyan} {reset} +Insert a new rule at the given index in the list. + +{*white}/autosort {brown}rules update {cyan} {reset} +Update a rule in the list with a new expression. + +{*white}/autosort {brown}rules delete {cyan} +Delete a rule from the list. + +{*white}/autosort {brown}rules move {cyan} {reset} +Move a rule from one position in the list to another. + +{*white}/autosort {brown}rules swap {cyan} {reset} +Swap two rules in the list + + +{*white}# Helper variable commands{reset} + +{*white}/autosort {brown}helpers list +Print the list of helper variables. + +{*white}/autosort {brown}helpers set {cyan} +Add or update a helper variable with the given name. + +{*white}/autosort {brown}helpers delete {cyan} +Delete a helper variable. + +{*white}/autosort {brown}helpers rename {cyan} +Rename a helper variable. + +{*white}/autosort {brown}helpers swap {cyan} +Swap the expressions of two helper variables in the list. + + +{*white}# Info hooks{reset} +Autosort comes with a number of info hooks to add some extra functionality to regular weechat eval strings. +Info hooks can be used in eval strings in the form of {cyan}${{info:some_hook,arguments}}{reset}. + +Commas and backslashes in arguments to autosort info hooks (except for {cyan}${{info:autosort_escape}}{reset}) must be escaped with a backslash. + +{*white}${{info:{brown}autosort_replace{white},{cyan}pattern{white},{cyan}replacement{white},{cyan}source{white}}}{reset} +Replace all occurrences of {cyan}pattern{reset} with {cyan}replacement{reset} in the string {cyan}source{reset}. +Can be used to ignore certain strings when sorting by replacing them with an empty string. + +For example: {cyan}${{info:autosort_replace,cat,dog,the dog is meowing}}{reset} expands to "the cat is meowing". + +{*white}${{info:{brown}autosort_order{white},{cyan}value{white},{cyan}option0{white},{cyan}option1{white},{cyan}option2{white},{cyan}...{white}}} +Generate a zero-padded number that corresponds to the index of {cyan}value{reset} in the list of options. +If one of the options is the special value {brown}*{reset}, then any value not explicitly mentioned will be sorted at that position. +Otherwise, any value that does not match an option is assigned the highest number available. +Can be used to easily sort buffers based on a manual sequence. + +For example: {cyan}${{info:autosort_order,${{server}},freenode,oftc,efnet}}{reset} will sort freenode before oftc, followed by efnet and then any remaining servers. +Alternatively, {cyan}${{info:autosort_order,${{server}},freenode,oftc,*,efnet}}{reset} will sort any unlisted servers after freenode and oftc, but before efnet. + +{*white}${{info:{brown}autosort_escape{white},{cyan}text{white}}}{reset} +Escape commas and backslashes in {cyan}text{reset} by prepending them with a backslash. +This is mainly useful to pass arbitrary eval strings as arguments to other autosort info hooks. +Otherwise, an eval string that expands to something with a comma would be interpreted as multiple arguments. + +For example, it can be used to safely pass buffer names to {cyan}${{info:autosort_replace}}{reset} like so: +{cyan}${{info:autosort_replace,##,#,${{info:autosort_escape,${{buffer.name}}}}}}{reset}. + + +{*white}# Description +Autosort is a weechat script to automatically keep your buffers sorted. The sort +order can be customized by defining your own sort rules, but the default should +be sane enough for most people. It can also group IRC channel/private buffers +under their server buffer if you like. + +Autosort uses a stable sorting algorithm, meaning that you can manually move buffers +to change their relative order, if they sort equal with your rule set. + +{*white}# Sort rules{reset} +Autosort evaluates a list of eval expressions (see {*default}/help eval{reset}) and sorts the +buffers based on evaluated result. Earlier rules will be considered first. Only +if earlier rules produced identical results is the result of the next rule +considered for sorting purposes. + +You can debug your sort rules with the `{*default}/autosort debug{reset}` command, which will +print the evaluation results of each rule for each buffer. + +{*brown}NOTE:{reset} The sort rules for version 3 are not compatible with version 2 or vice +versa. You will have to manually port your old rules to version 3 if you have any. + +{*white}# Helper variables{reset} +You may define helper variables for the main sort rules to keep your rules +readable. They can be used in the main sort rules as variables. For example, +a helper variable named `{cyan}foo{reset}` can be accessed in a main rule with the +string `{cyan}${{foo}}{reset}`. + +{*white}# Automatic or manual sorting{reset} +By default, autosort will automatically sort your buffer list whenever a buffer +is opened, merged, unmerged or renamed. This should keep your buffers sorted in +almost all situations. However, you may wish to change the list of signals that +cause your buffer list to be sorted. Simply edit the `{cyan}autosort.sorting.signals{reset}` +option to add or remove any signal you like. + +If you remove all signals you can still sort your buffers manually with the +`{*default}/autosort sort{reset}` command. To prevent all automatic sorting, the option +`{cyan}autosort.sorting.sort_on_config_change{reset}` should also be disabled. + +{*white}# Recommended settings +For the best visual effect, consider setting the following options: + {*white}/set {cyan}irc.look.server_buffer{reset} {brown}independent{reset} + +This setting allows server buffers to be sorted independently, which is +needed to create a hierarchical tree view of the server and channel buffers. + +If you are using the {*default}buflist{reset} plugin you can (ab)use Unicode to draw a tree +structure with the following setting (modify to suit your need): + {*white}/set {cyan}buflist.format.indent {brown}"${{color:237}}${{if:${{buffer.next_buffer.local_variables.type}}=~^(channel|private)$?├─:└─}}"{reset} +''' + +command_completion = '%(plugin_autosort) %(plugin_autosort) %(plugin_autosort) %(plugin_autosort) %(plugin_autosort)' + +info_replace_description = ( + 'Replace all occurrences of `pattern` with `replacement` in the string `source`. ' + 'Can be used to ignore certain strings when sorting by replacing them with an empty string. ' + 'See /help autosort for examples.' +) +info_replace_arguments = 'pattern,replacement,source' + +info_order_description = ( + 'Generate a zero-padded number that corresponds to the index of `value` in the list of options. ' + 'If one of the options is the special value `*`, then any value not explicitly mentioned will be sorted at that position. ' + 'Otherwise, any value that does not match an option is assigned the highest number available. ' + 'Can be used to easily sort buffers based on a manual sequence. ' + 'See /help autosort for examples.' +) +info_order_arguments = 'value,first,second,third,...' + +info_escape_description = ( + 'Escape commas and backslashes in `text` by prepending them with a backslash. ' + 'This is mainly useful to pass arbitrary eval strings as arguments to other autosort info hooks. ' + 'Otherwise, an eval string that expands to something with a comma would be interpreted as multiple arguments.' + 'See /help autosort for examples.' +) +info_escape_arguments = 'text' + + +if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): + config = Config('autosort') + + colors = { + 'default': weechat.color('default'), + 'reset': weechat.color('reset'), + 'black': weechat.color('black'), + 'red': weechat.color('red'), + 'green': weechat.color('green'), + 'brown': weechat.color('brown'), + 'yellow': weechat.color('yellow'), + 'blue': weechat.color('blue'), + 'magenta': weechat.color('magenta'), + 'cyan': weechat.color('cyan'), + 'white': weechat.color('white'), + '*default': weechat.color('*default'), + '*black': weechat.color('*black'), + '*red': weechat.color('*red'), + '*green': weechat.color('*green'), + '*brown': weechat.color('*brown'), + '*yellow': weechat.color('*yellow'), + '*blue': weechat.color('*blue'), + '*magenta': weechat.color('*magenta'), + '*cyan': weechat.color('*cyan'), + '*white': weechat.color('*white'), + } + + weechat.hook_config('autosort.*', 'on_config_changed', '') + weechat.hook_completion('plugin_autosort', '', 'on_autosort_complete', '') + weechat.hook_command('autosort', command_description.format(**colors), '', '', command_completion, 'on_autosort_command', '') + weechat.hook_info('autosort_escape', info_escape_description, info_escape_arguments, 'on_info_escape', '') + weechat.hook_info('autosort_replace', info_replace_description, info_replace_arguments, 'on_info_replace', '') + weechat.hook_info('autosort_order', info_order_description, info_order_arguments, 'on_info_order', '') + + apply_config() diff --git a/.config/weechat/python/autoload/colorize_nicks.py b/.config/weechat/python/autoload/colorize_nicks.py deleted file mode 120000 index 3ee34e96..00000000 --- a/.config/weechat/python/autoload/colorize_nicks.py +++ /dev/null @@ -1 +0,0 @@ -../colorize_nicks.py \ No newline at end of file diff --git a/.config/weechat/python/autoload/colorize_nicks.py b/.config/weechat/python/autoload/colorize_nicks.py new file mode 100644 index 00000000..cb95a0d6 --- /dev/null +++ b/.config/weechat/python/autoload/colorize_nicks.py @@ -0,0 +1,409 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2010 by xt +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This script colors nicks in IRC channels in the actual message +# not just in the prefix section. +# +# +# History: +# 2020-11-29: jess +# version 28: fix ignore_tags having been broken by weechat 2.9 changes +# 2020-05-09: Sébastien Helleu +# version 27: add compatibility with new weechat_print modifier data +# (WeeChat >= 2.9) +# 2018-04-06: Joey Pabalinas +# version 26: fix freezes with too many nicks in one line +# 2018-03-18: nils_2 +# version 25: fix unable to run function colorize_config_reload_cb() +# 2017-06-20: lbeziaud +# version 24: colorize utf8 nicks +# 2017-03-01, arza +# version 23: don't colorize nicklist group names +# 2016-05-01, Simmo Saan +# version 22: invalidate cached colors on hash algorithm change +# 2015-07-28, xt +# version 21: fix problems with nicks with commas in them +# 2015-04-19, xt +# version 20: fix ignore of nicks in URLs +# 2015-04-18, xt +# version 19: new option ignore nicks in URLs +# 2015-03-03, xt +# version 18: iterate buffers looking for nicklists instead of servers +# 2015-02-23, holomorph +# version 17: fix coloring in non-channel buffers (#58) +# 2014-09-17, holomorph +# version 16: use weechat config facilities +# clean unused, minor linting, some simplification +# 2014-05-05, holomorph +# version 15: fix python2-specific re.search check +# 2013-01-29, nils_2 +# version 14: make script compatible with Python 3.x +# 2012-10-19, ldvx +# version 13: Iterate over every word to prevent incorrect colorization of +# nicks. Added option greedy_matching. +# 2012-04-28, ldvx +# version 12: added ignore_tags to avoid colorizing nicks if tags are present +# 2012-01-14, nesthib +# version 11: input_text_display hook and modifier to colorize nicks in input bar +# 2010-12-22, xt +# version 10: hook config option for updating blacklist +# 2010-12-20, xt +# version 0.9: hook new config option for weechat 0.3.4 +# 2010-11-01, nils_2 +# version 0.8: hook_modifier() added to communicate with rainbow_text +# 2010-10-01, xt +# version 0.7: changes to support non-irc-plugins +# 2010-07-29, xt +# version 0.6: compile regexp as per patch from Chris quigybo@hotmail.com +# 2010-07-19, xt +# version 0.5: fix bug with incorrect coloring of own nick +# 2010-06-02, xt +# version 0.4: update to reflect API changes +# 2010-03-26, xt +# version 0.3: fix error with exception +# 2010-03-24, xt +# version 0.2: use ignore_channels when populating to increase performance. +# 2010-02-03, xt +# version 0.1: initial (based on ruby script by dominikh) +# +# Known issues: nicks will not get colorized if they begin with a character +# such as ~ (which some irc networks do happen to accept) + +import weechat +import re +w = weechat + +SCRIPT_NAME = "colorize_nicks" +SCRIPT_AUTHOR = "xt " +SCRIPT_VERSION = "28" +SCRIPT_LICENSE = "GPL" +SCRIPT_DESC = "Use the weechat nick colors in the chat area" + +# Based on the recommendations in RFC 7613. A valid nick is composed +# of anything but " ,*?.!@". +VALID_NICK = r'([@~&!%+-])?([^\s,\*?\.!@]+)' +valid_nick_re = re.compile(VALID_NICK) +ignore_channels = [] +ignore_nicks = [] + +# Dict with every nick on every channel with its color as lookup value +colored_nicks = {} + +CONFIG_FILE_NAME = "colorize_nicks" + +# config file and options +colorize_config_file = "" +colorize_config_option = {} + +def colorize_config_init(): + ''' + Initialization of configuration file. + Sections: look. + ''' + global colorize_config_file, colorize_config_option + colorize_config_file = weechat.config_new(CONFIG_FILE_NAME, + "", "") + if colorize_config_file == "": + return + + # section "look" + section_look = weechat.config_new_section( + colorize_config_file, "look", 0, 0, "", "", "", "", "", "", "", "", "", "") + if section_look == "": + weechat.config_free(colorize_config_file) + return + colorize_config_option["blacklist_channels"] = weechat.config_new_option( + colorize_config_file, section_look, "blacklist_channels", + "string", "Comma separated list of channels", "", 0, 0, + "", "", 0, "", "", "", "", "", "") + colorize_config_option["blacklist_nicks"] = weechat.config_new_option( + colorize_config_file, section_look, "blacklist_nicks", + "string", "Comma separated list of nicks", "", 0, 0, + "so,root", "so,root", 0, "", "", "", "", "", "") + colorize_config_option["min_nick_length"] = weechat.config_new_option( + colorize_config_file, section_look, "min_nick_length", + "integer", "Minimum length nick to colorize", "", + 2, 20, "", "", 0, "", "", "", "", "", "") + colorize_config_option["colorize_input"] = weechat.config_new_option( + colorize_config_file, section_look, "colorize_input", + "boolean", "Whether to colorize input", "", 0, + 0, "off", "off", 0, "", "", "", "", "", "") + colorize_config_option["ignore_tags"] = weechat.config_new_option( + colorize_config_file, section_look, "ignore_tags", + "string", "Comma separated list of tags to ignore; i.e. irc_join,irc_part,irc_quit", "", 0, 0, + "", "", 0, "", "", "", "", "", "") + colorize_config_option["greedy_matching"] = weechat.config_new_option( + colorize_config_file, section_look, "greedy_matching", + "boolean", "If off, then use lazy matching instead", "", 0, + 0, "on", "on", 0, "", "", "", "", "", "") + colorize_config_option["match_limit"] = weechat.config_new_option( + colorize_config_file, section_look, "match_limit", + "integer", "Fall back to lazy matching if greedy matches exceeds this number", "", + 20, 1000, "", "", 0, "", "", "", "", "", "") + colorize_config_option["ignore_nicks_in_urls"] = weechat.config_new_option( + colorize_config_file, section_look, "ignore_nicks_in_urls", + "boolean", "If on, don't colorize nicks inside URLs", "", 0, + 0, "off", "off", 0, "", "", "", "", "", "") + +def colorize_config_read(): + ''' Read configuration file. ''' + global colorize_config_file + return weechat.config_read(colorize_config_file) + +def colorize_nick_color(nick, my_nick): + ''' Retrieve nick color from weechat. ''' + if nick == my_nick: + return w.color(w.config_string(w.config_get('weechat.color.chat_nick_self'))) + else: + return w.info_get('irc_nick_color', nick) + +def colorize_cb(data, modifier, modifier_data, line): + ''' Callback that does the colorizing, and returns new line if changed ''' + + global ignore_nicks, ignore_channels, colored_nicks + + if modifier_data.startswith('0x'): + # WeeChat >= 2.9 + buffer, tags = modifier_data.split(';', 1) + else: + # WeeChat <= 2.8 + plugin, buffer_name, tags = modifier_data.split(';', 2) + buffer = w.buffer_search(plugin, buffer_name) + + channel = w.buffer_get_string(buffer, 'localvar_channel') + tags = tags.split(',') + + # Check if buffer has colorized nicks + if buffer not in colored_nicks: + return line + + if channel and channel in ignore_channels: + return line + + min_length = w.config_integer(colorize_config_option['min_nick_length']) + reset = w.color('reset') + + # Don't colorize if the ignored tag is present in message + tag_ignores = w.config_string(colorize_config_option['ignore_tags']).split(',') + for tag in tags: + if tag in tag_ignores: + return line + + for words in valid_nick_re.findall(line): + nick = words[1] + # Check that nick is not ignored and longer than minimum length + if len(nick) < min_length or nick in ignore_nicks: + continue + + # If the matched word is not a known nick, we try to match the + # word without its first or last character (if not a letter). + # This is necessary as "foo:" is a valid nick, which could be + # adressed as "foo::". + if nick not in colored_nicks[buffer]: + if not nick[-1].isalpha() and not nick[0].isalpha(): + if nick[1:-1] in colored_nicks[buffer]: + nick = nick[1:-1] + elif not nick[0].isalpha(): + if nick[1:] in colored_nicks[buffer]: + nick = nick[1:] + elif not nick[-1].isalpha(): + if nick[:-1] in colored_nicks[buffer]: + nick = nick[:-1] + + # Check that nick is in the dictionary colored_nicks + if nick in colored_nicks[buffer]: + nick_color = colored_nicks[buffer][nick] + + try: + # Let's use greedy matching. Will check against every word in a line. + if w.config_boolean(colorize_config_option['greedy_matching']): + cnt = 0 + limit = w.config_integer(colorize_config_option['match_limit']) + + for word in line.split(): + cnt += 1 + assert cnt < limit + # if cnt > limit: + # raise RuntimeError('Exceeded colorize_nicks.look.match_limit.'); + + if w.config_boolean(colorize_config_option['ignore_nicks_in_urls']) and \ + word.startswith(('http://', 'https://')): + continue + + if nick in word: + # Is there a nick that contains nick and has a greater lenght? + # If so let's save that nick into var biggest_nick + biggest_nick = "" + for i in colored_nicks[buffer]: + cnt += 1 + assert cnt < limit + + if nick in i and nick != i and len(i) > len(nick): + if i in word: + # If a nick with greater len is found, and that word + # also happens to be in word, then let's save this nick + biggest_nick = i + # If there's a nick with greater len, then let's skip this + # As we will have the chance to colorize when biggest_nick + # iterates being nick. + if len(biggest_nick) > 0 and biggest_nick in word: + pass + elif len(word) < len(biggest_nick) or len(biggest_nick) == 0: + new_word = word.replace(nick, '%s%s%s' % (nick_color, nick, reset)) + line = line.replace(word, new_word) + + # Switch to lazy matching + else: + raise AssertionError + + except AssertionError: + # Let's use lazy matching for nick + nick_color = colored_nicks[buffer][nick] + # The two .? are in case somebody writes "nick:", "nick,", etc + # to address somebody + regex = r"(\A|\s).?(%s).?(\Z|\s)" % re.escape(nick) + match = re.search(regex, line) + if match is not None: + new_line = line[:match.start(2)] + nick_color+nick+reset + line[match.end(2):] + line = new_line + + return line + +def colorize_input_cb(data, modifier, modifier_data, line): + ''' Callback that does the colorizing in input ''' + + global ignore_nicks, ignore_channels, colored_nicks + + min_length = w.config_integer(colorize_config_option['min_nick_length']) + + if not w.config_boolean(colorize_config_option['colorize_input']): + return line + + buffer = w.current_buffer() + # Check if buffer has colorized nicks + if buffer not in colored_nicks: + return line + + channel = w.buffer_get_string(buffer, 'name') + if channel and channel in ignore_channels: + return line + + reset = w.color('reset') + + for words in valid_nick_re.findall(line): + nick = words[1] + # Check that nick is not ignored and longer than minimum length + if len(nick) < min_length or nick in ignore_nicks: + continue + if nick in colored_nicks[buffer]: + nick_color = colored_nicks[buffer][nick] + line = line.replace(nick, '%s%s%s' % (nick_color, nick, reset)) + + return line + +def populate_nicks(*args): + ''' Fills entire dict with all nicks weechat can see and what color it has + assigned to it. ''' + global colored_nicks + + colored_nicks = {} + + buffers = w.infolist_get('buffer', '', '') + while w.infolist_next(buffers): + buffer_ptr = w.infolist_pointer(buffers, 'pointer') + my_nick = w.buffer_get_string(buffer_ptr, 'localvar_nick') + nicklist = w.infolist_get('nicklist', buffer_ptr, '') + while w.infolist_next(nicklist): + if buffer_ptr not in colored_nicks: + colored_nicks[buffer_ptr] = {} + + if w.infolist_string(nicklist, 'type') != 'nick': + continue + + nick = w.infolist_string(nicklist, 'name') + nick_color = colorize_nick_color(nick, my_nick) + + colored_nicks[buffer_ptr][nick] = nick_color + + w.infolist_free(nicklist) + + w.infolist_free(buffers) + + return w.WEECHAT_RC_OK + +def add_nick(data, signal, type_data): + ''' Add nick to dict of colored nicks ''' + global colored_nicks + + # Nicks can have , in them in some protocols + splitted = type_data.split(',') + pointer = splitted[0] + nick = ",".join(splitted[1:]) + if pointer not in colored_nicks: + colored_nicks[pointer] = {} + + my_nick = w.buffer_get_string(pointer, 'localvar_nick') + nick_color = colorize_nick_color(nick, my_nick) + + colored_nicks[pointer][nick] = nick_color + + return w.WEECHAT_RC_OK + +def remove_nick(data, signal, type_data): + ''' Remove nick from dict with colored nicks ''' + global colored_nicks + + # Nicks can have , in them in some protocols + splitted = type_data.split(',') + pointer = splitted[0] + nick = ",".join(splitted[1:]) + + if pointer in colored_nicks and nick in colored_nicks[pointer]: + del colored_nicks[pointer][nick] + + return w.WEECHAT_RC_OK + +def update_blacklist(*args): + ''' Set the blacklist for channels and nicks. ''' + global ignore_channels, ignore_nicks + ignore_channels = w.config_string(colorize_config_option['blacklist_channels']).split(',') + ignore_nicks = w.config_string(colorize_config_option['blacklist_nicks']).split(',') + return w.WEECHAT_RC_OK + +if __name__ == "__main__": + if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, + SCRIPT_DESC, "", ""): + colorize_config_init() + colorize_config_read() + + # Run once to get data ready + update_blacklist() + populate_nicks() + + w.hook_signal('nicklist_nick_added', 'add_nick', '') + w.hook_signal('nicklist_nick_removed', 'remove_nick', '') + w.hook_modifier('weechat_print', 'colorize_cb', '') + # Hook config for changing colors + w.hook_config('weechat.color.chat_nick_colors', 'populate_nicks', '') + w.hook_config('weechat.look.nick_color_hash', 'populate_nicks', '') + # Hook for working togheter with other scripts (like colorize_lines) + w.hook_modifier('colorize_nicks', 'colorize_cb', '') + # Hook for modifying input + w.hook_modifier('250|input_text_display', 'colorize_input_cb', '') + # Hook for updating blacklist (this could be improved to use fnmatch) + weechat.hook_config('%s.look.blacklist*' % SCRIPT_NAME, 'update_blacklist', '') diff --git a/.config/weechat/python/autoload/go.py b/.config/weechat/python/autoload/go.py deleted file mode 120000 index bdfb7ddf..00000000 --- a/.config/weechat/python/autoload/go.py +++ /dev/null @@ -1 +0,0 @@ -../go.py \ No newline at end of file diff --git a/.config/weechat/python/autoload/go.py b/.config/weechat/python/autoload/go.py new file mode 100644 index 00000000..2ab47ed4 --- /dev/null +++ b/.config/weechat/python/autoload/go.py @@ -0,0 +1,563 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2009-2014 Sébastien Helleu +# Copyright (C) 2010 m4v +# Copyright (C) 2011 stfn +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# +# History: +# +# 2019-07-11, Simmo Saan +# version 2.6: fix detection of "/input search_text_here" +# 2017-04-01, Sébastien Helleu : +# version 2.5: add option "buffer_number" +# 2017-03-02, Sébastien Helleu : +# version 2.4: fix syntax and indentation error +# 2017-02-25, Simmo Saan +# version 2.3: fix fuzzy search breaking buffer number search display +# 2016-01-28, ylambda +# version 2.2: add option "fuzzy_search" +# 2015-11-12, nils_2 +# version 2.1: fix problem with buffer short_name "weechat", using option +# "use_core_instead_weechat", see: +# https://github.com/weechat/weechat/issues/574 +# 2014-05-12, Sébastien Helleu : +# version 2.0: add help on options, replace option "sort_by_activity" by +# "sort" (add sort by name and first match at beginning of +# name and by number), PEP8 compliance +# 2012-11-26, Nei +# version 1.9: add auto_jump option to automatically go to buffer when it +# is uniquely selected +# 2012-09-17, Sébastien Helleu : +# version 1.8: fix jump to non-active merged buffers (jump with buffer name +# instead of number) +# 2012-01-03 nils_2 +# version 1.7: add option "use_core_instead_weechat" +# 2012-01-03, Sébastien Helleu : +# version 1.6: make script compatible with Python 3.x +# 2011-08-24, stfn : +# version 1.5: /go with name argument jumps directly to buffer +# Remember cursor position in buffer input +# 2011-05-31, Elián Hanisch : +# version 1.4: Sort list of buffers by activity. +# 2011-04-25, Sébastien Helleu : +# version 1.3: add info "go_running" (used by script input_lock.rb) +# 2010-11-01, Sébastien Helleu : +# version 1.2: use high priority for hooks to prevent conflict with other +# plugins/scripts (WeeChat >= 0.3.4 only) +# 2010-03-25, Elián Hanisch : +# version 1.1: use a space to match the end of a string +# 2009-11-16, Sébastien Helleu : +# version 1.0: add new option to display short names +# 2009-06-15, Sébastien Helleu : +# version 0.9: fix typo in /help go with command /key +# 2009-05-16, Sébastien Helleu : +# version 0.8: search buffer by number, fix bug when window is split +# 2009-05-03, Sébastien Helleu : +# version 0.7: eat tab key (do not complete input, just move buffer +# pointer) +# 2009-05-02, Sébastien Helleu : +# version 0.6: sync with last API changes +# 2009-03-22, Sébastien Helleu : +# version 0.5: update modifier signal name for input text display, +# fix arguments for function string_remove_color +# 2009-02-18, Sébastien Helleu : +# version 0.4: do not hook command and init options if register failed +# 2009-02-08, Sébastien Helleu : +# version 0.3: case insensitive search for buffers names +# 2009-02-08, Sébastien Helleu : +# version 0.2: add help about Tab key +# 2009-02-08, Sébastien Helleu : +# version 0.1: initial release +# + +""" +Quick jump to buffers. +(this script requires WeeChat 0.3.0 or newer) +""" + +from __future__ import print_function + +SCRIPT_NAME = 'go' +SCRIPT_AUTHOR = 'Sébastien Helleu ' +SCRIPT_VERSION = '2.6' +SCRIPT_LICENSE = 'GPL3' +SCRIPT_DESC = 'Quick jump to buffers' + +SCRIPT_COMMAND = 'go' + +IMPORT_OK = True + +try: + import weechat +except ImportError: + print('This script must be run under WeeChat.') + print('Get WeeChat now at: http://www.weechat.org/') + IMPORT_OK = False + +import re + +# script options +SETTINGS = { + 'color_number': ( + 'yellow,magenta', + 'color for buffer number (not selected)'), + 'color_number_selected': ( + 'yellow,red', + 'color for selected buffer number'), + 'color_name': ( + 'black,cyan', + 'color for buffer name (not selected)'), + 'color_name_selected': ( + 'black,brown', + 'color for a selected buffer name'), + 'color_name_highlight': ( + 'red,cyan', + 'color for highlight in buffer name (not selected)'), + 'color_name_highlight_selected': ( + 'red,brown', + 'color for highlight in a selected buffer name'), + 'message': ( + 'Go to: ', + 'message to display before list of buffers'), + 'short_name': ( + 'off', + 'display and search in short names instead of buffer name'), + 'sort': ( + 'number,beginning', + 'comma-separated list of keys to sort buffers ' + '(the order is important, sorts are performed in the given order): ' + 'name = sort by name (or short name), ', + 'hotlist = sort by hotlist order, ' + 'number = first match a buffer number before digits in name, ' + 'beginning = first match at beginning of names (or short names); ' + 'the default sort of buffers is by numbers'), + 'use_core_instead_weechat': ( + 'off', + 'use name "core" instead of "weechat" for core buffer'), + 'auto_jump': ( + 'off', + 'automatically jump to buffer when it is uniquely selected'), + 'fuzzy_search': ( + 'off', + 'search buffer matches using approximation'), + 'buffer_number': ( + 'on', + 'display buffer number'), +} + +# hooks management +HOOK_COMMAND_RUN = { + 'input': ('/input *', 'go_command_run_input'), + 'buffer': ('/buffer *', 'go_command_run_buffer'), + 'window': ('/window *', 'go_command_run_window'), +} +hooks = {} + +# input before command /go (we'll restore it later) +saved_input = '' +saved_input_pos = 0 + +# last user input (if changed, we'll update list of matching buffers) +old_input = None + +# matching buffers +buffers = [] +buffers_pos = 0 + + +def go_option_enabled(option): + """Checks if a boolean script option is enabled or not.""" + return weechat.config_string_to_boolean(weechat.config_get_plugin(option)) + + +def go_info_running(data, info_name, arguments): + """Returns "1" if go is running, otherwise "0".""" + return '1' if 'modifier' in hooks else '0' + + +def go_unhook_one(hook): + """Unhook something hooked by this script.""" + global hooks + if hook in hooks: + weechat.unhook(hooks[hook]) + del hooks[hook] + + +def go_unhook_all(): + """Unhook all.""" + go_unhook_one('modifier') + for hook in HOOK_COMMAND_RUN: + go_unhook_one(hook) + + +def go_hook_all(): + """Hook command_run and modifier.""" + global hooks + priority = '' + version = weechat.info_get('version_number', '') or 0 + # use high priority for hook to prevent conflict with other plugins/scripts + # (WeeChat >= 0.3.4 only) + if int(version) >= 0x00030400: + priority = '2000|' + for hook, value in HOOK_COMMAND_RUN.items(): + if hook not in hooks: + hooks[hook] = weechat.hook_command_run( + '%s%s' % (priority, value[0]), + value[1], '') + if 'modifier' not in hooks: + hooks['modifier'] = weechat.hook_modifier( + 'input_text_display_with_cursor', 'go_input_modifier', '') + + +def go_start(buf): + """Start go on buffer.""" + global saved_input, saved_input_pos, old_input, buffers_pos + go_hook_all() + saved_input = weechat.buffer_get_string(buf, 'input') + saved_input_pos = weechat.buffer_get_integer(buf, 'input_pos') + weechat.buffer_set(buf, 'input', '') + old_input = None + buffers_pos = 0 + + +def go_end(buf): + """End go on buffer.""" + global saved_input, saved_input_pos, old_input + go_unhook_all() + weechat.buffer_set(buf, 'input', saved_input) + weechat.buffer_set(buf, 'input_pos', str(saved_input_pos)) + old_input = None + + +def go_match_beginning(buf, string): + """Check if a string matches the beginning of buffer name/short name.""" + if not string: + return False + esc_str = re.escape(string) + if re.search(r'^#?' + esc_str, buf['name']) \ + or re.search(r'^#?' + esc_str, buf['short_name']): + return True + return False + + +def go_match_fuzzy(name, string): + """Check if string matches name using approximation.""" + if not string: + return False + + name_len = len(name) + string_len = len(string) + + if string_len > name_len: + return False + if name_len == string_len: + return name == string + + # Attempt to match all chars somewhere in name + prev_index = -1 + for i, char in enumerate(string): + index = name.find(char, prev_index+1) + if index == -1: + return False + prev_index = index + return True + + +def go_now(buf, args): + """Go to buffer specified by args.""" + listbuf = go_matching_buffers(args) + if not listbuf: + return + + # prefer buffer that matches at beginning (if option is enabled) + if 'beginning' in weechat.config_get_plugin('sort').split(','): + for index in range(len(listbuf)): + if go_match_beginning(listbuf[index], args): + weechat.command(buf, + '/buffer ' + str(listbuf[index]['full_name'])) + return + + # jump to first buffer in matching buffers by default + weechat.command(buf, '/buffer ' + str(listbuf[0]['full_name'])) + + +def go_cmd(data, buf, args): + """Command "/go": just hook what we need.""" + global hooks + if args: + go_now(buf, args) + elif 'modifier' in hooks: + go_end(buf) + else: + go_start(buf) + return weechat.WEECHAT_RC_OK + + +def go_matching_buffers(strinput): + """Return a list with buffers matching user input.""" + global buffers_pos + listbuf = [] + if len(strinput) == 0: + buffers_pos = 0 + strinput = strinput.lower() + infolist = weechat.infolist_get('buffer', '', '') + while weechat.infolist_next(infolist): + short_name = weechat.infolist_string(infolist, 'short_name') + if go_option_enabled('short_name'): + name = weechat.infolist_string(infolist, 'short_name') + else: + name = weechat.infolist_string(infolist, 'name') + if name == 'weechat' \ + and go_option_enabled('use_core_instead_weechat') \ + and weechat.infolist_string(infolist, 'plugin_name') == 'core': + name = 'core' + number = weechat.infolist_integer(infolist, 'number') + full_name = weechat.infolist_string(infolist, 'full_name') + if not full_name: + full_name = '%s.%s' % ( + weechat.infolist_string(infolist, 'plugin_name'), + weechat.infolist_string(infolist, 'name')) + pointer = weechat.infolist_pointer(infolist, 'pointer') + matching = name.lower().find(strinput) >= 0 + if not matching and strinput[-1] == ' ': + matching = name.lower().endswith(strinput.strip()) + if not matching and go_option_enabled('fuzzy_search'): + matching = go_match_fuzzy(name.lower(), strinput) + if not matching and strinput.isdigit(): + matching = str(number).startswith(strinput) + if len(strinput) == 0 or matching: + listbuf.append({ + 'number': number, + 'short_name': short_name, + 'name': name, + 'full_name': full_name, + 'pointer': pointer, + }) + weechat.infolist_free(infolist) + + # sort buffers + hotlist = [] + infolist = weechat.infolist_get('hotlist', '', '') + while weechat.infolist_next(infolist): + hotlist.append( + weechat.infolist_pointer(infolist, 'buffer_pointer')) + weechat.infolist_free(infolist) + last_index_hotlist = len(hotlist) + + def _sort_name(buf): + """Sort buffers by name (or short name).""" + return buf['name'] + + def _sort_hotlist(buf): + """Sort buffers by hotlist order.""" + try: + return hotlist.index(buf['pointer']) + except ValueError: + # not in hotlist, always last. + return last_index_hotlist + + def _sort_match_number(buf): + """Sort buffers by match on number.""" + return 0 if str(buf['number']) == strinput else 1 + + def _sort_match_beginning(buf): + """Sort buffers by match at beginning.""" + return 0 if go_match_beginning(buf, strinput) else 1 + + funcs = { + 'name': _sort_name, + 'hotlist': _sort_hotlist, + 'number': _sort_match_number, + 'beginning': _sort_match_beginning, + } + + for key in weechat.config_get_plugin('sort').split(','): + if key in funcs: + listbuf = sorted(listbuf, key=funcs[key]) + + if not strinput: + index = [i for i, buf in enumerate(listbuf) + if buf['pointer'] == weechat.current_buffer()] + if index: + buffers_pos = index[0] + + return listbuf + + +def go_buffers_to_string(listbuf, pos, strinput): + """Return string built with list of buffers found (matching user input).""" + string = '' + strinput = strinput.lower() + for i in range(len(listbuf)): + selected = '_selected' if i == pos else '' + buffer_name = listbuf[i]['name'] + index = buffer_name.lower().find(strinput) + if index >= 0: + index2 = index + len(strinput) + name = '%s%s%s%s%s' % ( + buffer_name[:index], + weechat.color(weechat.config_get_plugin( + 'color_name_highlight' + selected)), + buffer_name[index:index2], + weechat.color(weechat.config_get_plugin( + 'color_name' + selected)), + buffer_name[index2:]) + elif go_option_enabled("fuzzy_search") and \ + go_match_fuzzy(buffer_name.lower(), strinput): + name = "" + prev_index = -1 + for char in strinput.lower(): + index = buffer_name.lower().find(char, prev_index+1) + if prev_index < 0: + name += buffer_name[:index] + name += weechat.color(weechat.config_get_plugin( + 'color_name_highlight' + selected)) + if prev_index >= 0 and index > prev_index+1: + name += weechat.color(weechat.config_get_plugin( + 'color_name' + selected)) + name += buffer_name[prev_index+1:index] + name += weechat.color(weechat.config_get_plugin( + 'color_name_highlight' + selected)) + name += buffer_name[index] + prev_index = index + + name += weechat.color(weechat.config_get_plugin( + 'color_name' + selected)) + name += buffer_name[prev_index+1:] + else: + name = buffer_name + string += ' ' + if go_option_enabled('buffer_number'): + string += '%s%s' % ( + weechat.color(weechat.config_get_plugin( + 'color_number' + selected)), + str(listbuf[i]['number'])) + string += '%s%s%s' % ( + weechat.color(weechat.config_get_plugin( + 'color_name' + selected)), + name, + weechat.color('reset')) + return ' ' + string if string else '' + + +def go_input_modifier(data, modifier, modifier_data, string): + """This modifier is called when input text item is built by WeeChat. + + This is commonly called after changes in input or cursor move: it builds + a new input with prefix ("Go to:"), and suffix (list of buffers found). + """ + global old_input, buffers, buffers_pos + if modifier_data != weechat.current_buffer(): + return '' + names = '' + new_input = weechat.string_remove_color(string, '') + new_input = new_input.lstrip() + if old_input is None or new_input != old_input: + old_buffers = buffers + buffers = go_matching_buffers(new_input) + if buffers != old_buffers and len(new_input) > 0: + if len(buffers) == 1 and go_option_enabled('auto_jump'): + weechat.command(modifier_data, '/wait 1ms /input return') + buffers_pos = 0 + old_input = new_input + names = go_buffers_to_string(buffers, buffers_pos, new_input.strip()) + return weechat.config_get_plugin('message') + string + names + + +def go_command_run_input(data, buf, command): + """Function called when a command "/input xxx" is run.""" + global buffers, buffers_pos + if command.startswith('/input search_text') or command.startswith('/input jump'): + # search text or jump to another buffer is forbidden now + return weechat.WEECHAT_RC_OK_EAT + elif command == '/input complete_next': + # choose next buffer in list + buffers_pos += 1 + if buffers_pos >= len(buffers): + buffers_pos = 0 + weechat.hook_signal_send('input_text_changed', + weechat.WEECHAT_HOOK_SIGNAL_STRING, '') + return weechat.WEECHAT_RC_OK_EAT + elif command == '/input complete_previous': + # choose previous buffer in list + buffers_pos -= 1 + if buffers_pos < 0: + buffers_pos = len(buffers) - 1 + weechat.hook_signal_send('input_text_changed', + weechat.WEECHAT_HOOK_SIGNAL_STRING, '') + return weechat.WEECHAT_RC_OK_EAT + elif command == '/input return': + # switch to selected buffer (if any) + go_end(buf) + if len(buffers) > 0: + weechat.command( + buf, '/buffer ' + str(buffers[buffers_pos]['full_name'])) + return weechat.WEECHAT_RC_OK_EAT + return weechat.WEECHAT_RC_OK + + +def go_command_run_buffer(data, buf, command): + """Function called when a command "/buffer xxx" is run.""" + return weechat.WEECHAT_RC_OK_EAT + + +def go_command_run_window(data, buf, command): + """Function called when a command "/window xxx" is run.""" + return weechat.WEECHAT_RC_OK_EAT + + +def go_unload_script(): + """Function called when script is unloaded.""" + go_unhook_all() + return weechat.WEECHAT_RC_OK + + +def go_main(): + """Entry point.""" + if not weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, + SCRIPT_LICENSE, SCRIPT_DESC, + 'go_unload_script', ''): + return + weechat.hook_command( + SCRIPT_COMMAND, + 'Quick jump to buffers', '[name]', + 'name: directly jump to buffer by name (without argument, list is ' + 'displayed)\n\n' + 'You can bind command to a key, for example:\n' + ' /key bind meta-g /go\n\n' + 'You can use completion key (commonly Tab and shift-Tab) to select ' + 'next/previous buffer in list.', + '%(buffers_names)', + 'go_cmd', '') + + # set default settings + version = weechat.info_get('version_number', '') or 0 + for option, value in SETTINGS.items(): + if not weechat.config_is_set_plugin(option): + weechat.config_set_plugin(option, value[0]) + if int(version) >= 0x00030500: + weechat.config_set_desc_plugin( + option, '%s (default: "%s")' % (value[1], value[0])) + weechat.hook_info('go_running', + 'Return "1" if go is running, otherwise "0"', + '', + 'go_info_running', '') + + +if __name__ == "__main__" and IMPORT_OK: + go_main() diff --git a/.config/weechat/python/autoload/styurl.py b/.config/weechat/python/autoload/styurl.py deleted file mode 120000 index 30732091..00000000 --- a/.config/weechat/python/autoload/styurl.py +++ /dev/null @@ -1 +0,0 @@ -../styurl.py \ No newline at end of file diff --git a/.config/weechat/python/autoload/styurl.py b/.config/weechat/python/autoload/styurl.py new file mode 100644 index 00000000..69716505 --- /dev/null +++ b/.config/weechat/python/autoload/styurl.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2019 Cole Helbling +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# Changelog: +# 2019-12-14, Cole Helbling +# version 1.0: initial release + +SCRIPT_NAME = "styurl" +SCRIPT_AUTHOR = "Cole Helbling " +SCRIPT_VERSION = "1.0" +SCRIPT_LICENSE = "GPL3" +SCRIPT_DESC = "Style URLs with a Python regex" + +import_ok = True +try: + import weechat as w +except ImportError: + print("This script must be run under WeeChat.") + print("Get WeeChat now at: https://weechat.org") + import_ok = False + +try: + import re +except ImportError as message: + print("Missing package for %s: %s" % (SCRIPT_NAME, message)) + import_ok = False + +# https://mathiasbynens.be/demo/url-regex +# If you don't want to create your own regex, see the above link for options or +# ideas on creating your own + +styurl_settings = { + "buffer_type": ( + "formatted", + "the type of buffers to run on (options are \"formatted\", \"free\", " + "or \"*\" for both)" + ), + "format": ( + "${color:*_32}", + "the style that should be applied to the URL" + "(evaluated, see /help eval)" + ), + "ignored_buffers": ( + "core.weechat,python.grep", + "comma-separated list of buffers to ignore URLs in " + "(full name like \"irc.freenode.#alacritty\")" + ), + "ignored_tags": ( + "irc_quit,irc_join", + "comma-separated list of tags to ignore URLs from" + ), + "regex": ( + r"((?:https?|ftp)://[^\s/$.?#].\S*)", + "the URL-parsing regex using Python syntax " + "(make sure capturing group 1 is the full URL)" + ), +} + +line_hook = None + + +def styurl_line_cb(data, line): + """ + Callback called when a line is displayed. + This parses the message for any URLs and styles them according to + styurl_settings["format"]. + """ + global styurl_settings + + # Don't style the line if it's not going to be displayed... duh + if line["displayed"] != "1": + return line + + tags = line["tags"].split(',') + ignored_tags = styurl_settings["ignored_tags"] + + # Ignore specified message tags + if ignored_tags: + if any(tag in tags for tag in ignored_tags.split(',')): + return line + + bufname = line["buffer_name"] + ignored_buffers = styurl_settings["ignored_buffers"] + + # Ignore specified buffers + if ignored_buffers and bufname in ignored_buffers.split(','): + return line + + message = line["message"] + + # TODO: enforce presence of a properly-formatted color object at + # styurl_settings["format"] (eval object would also be valid, if it eval'd + # to a color) + + regex = re.compile(styurl_settings["regex"]) + url_style = w.string_eval_expression(styurl_settings["format"], {}, {}, {}) + reset = w.color("reset") + + # Search for URLs and surround them with the defined URL styling + formatted = regex.sub(r"%s\1%s" % (url_style, reset), message) + line["message"] = line["message"].replace(message, formatted) + + return line + + +def styurl_config_cb(data, option, value): + """Callback called when a script option is changed.""" + global styurl_settings, line_hook + + pos = option.rfind('.') + if pos > 0: + name = option[pos+1:] + if name in styurl_settings: + # Changing the buffer target requires us to re-hook to prevent + # obsolete buffer types from getting styled + if name == "buffer_type": + if value in ("free", "formatted", "*"): + w.unhook(line_hook) + line_hook = w.hook_line(value, "", "", "styurl_line_cb", + "") + else: + # Don't change buffer type if it is invalid + w.prnt("", SCRIPT_NAME + ": Invalid buffer type: '%s', " + "not changing." % value) + w.config_set_plugin(name, styurl_settings[name]) + return w.WEECHAT_RC_ERROR + + styurl_settings[name] = value + + return w.WEECHAT_RC_OK + + +def styurl_unload_cb(): + """Callback called when the script is unloaded.""" + global line_hook + + w.unhook(line_hook) + del line_hook + return w.WEECHAT_RC_OK + + +if __name__ == "__main__" and import_ok: + if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, + SCRIPT_DESC, "styurl_unload_cb", ""): + + version = w.info_get("version_number", "") or 0 + + for option, value in styurl_settings.items(): + if w.config_is_set_plugin(option): + styurl_settings[option] = w.config_get_plugin(option) + else: + w.config_set_plugin(option, value[0]) + styurl_settings[option] = value[0] + if int(version) >= 0x00030500: + w.config_set_desc_plugin(option, "%s (default: \"%s\")" + % (value[1], value[0])) + + w.hook_config("plugins.var.python." + SCRIPT_NAME + ".*", + "styurl_config_cb", "") + + # Style URLs + line_hook = w.hook_line(styurl_settings["buffer_type"], "", "", + "styurl_line_cb", "") diff --git a/.config/weechat/python/autoload/vimode.py b/.config/weechat/python/autoload/vimode.py deleted file mode 120000 index c6303a5c..00000000 --- a/.config/weechat/python/autoload/vimode.py +++ /dev/null @@ -1 +0,0 @@ -../vimode.py \ No newline at end of file diff --git a/.config/weechat/python/autoload/vimode.py b/.config/weechat/python/autoload/vimode.py new file mode 100644 index 00000000..b1e66410 --- /dev/null +++ b/.config/weechat/python/autoload/vimode.py @@ -0,0 +1,1884 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013-2014 Germain Z. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# +# Add vi/vim-like modes to WeeChat. +# + + +import csv +import json +import os +import re +import subprocess +try: + from StringIO import StringIO +except ImportError: + from io import StringIO +import time + +import weechat + + +# Script info. +# ============ + +SCRIPT_NAME = "vimode" +SCRIPT_AUTHOR = "GermainZ " +SCRIPT_VERSION = "0.8.1" +SCRIPT_LICENSE = "GPL3" +SCRIPT_DESC = ("Add vi/vim-like modes and keybindings to WeeChat.") + + +# Global variables. +# ================= + +# General. +# -------- + +# Halp! Halp! Halp! +GITHUB_BASE = "https://github.com/GermainZ/weechat-vimode/blob/master/" +README_URL = GITHUB_BASE + "README.md" +FAQ_KEYBINDINGS = GITHUB_BASE + "FAQ.md#problematic-key-bindings" +FAQ_ESC = GITHUB_BASE + "FAQ.md#esc-key-not-being-detected-instantly" + +# Holds the text of the tab-completions for the command-line mode. +cmd_compl_text = "" +# Holds the original text of the command-line mode, used for completion. +cmd_text_orig = None +# Index of current suggestion, used for completion. +cmd_compl_pos = 0 +# Used for command-line mode history. +cmd_history = [] +cmd_history_index = 0 +# Used to store the content of the input line when going into COMMAND mode. +input_line_backup = {} +# Mode we're in. One of INSERT, NORMAL, REPLACE, COMMAND or SEARCH. +# SEARCH is only used if search_vim is enabled. +mode = "INSERT" +# Holds normal commands (e.g. "dd"). +vi_buffer = "" +# See `cb_key_combo_default()`. +esc_pressed = 0 +# See `cb_key_pressed()`. +last_signal_time = 0 +# See `start_catching_keys()` for more info. +catching_keys_data = {'amount': 0} +# Used for ; and , to store the last f/F/t/T motion. +last_search_motion = {'motion': None, 'data': None} +# Used for undo history. +undo_history = {} +undo_history_index = {} +# Holds mode colors (loaded from vimode_settings). +mode_colors = {} + +# Script options. +vimode_settings = { + 'no_warn': ("off", ("don't warn about problematic keybindings and " + "tmux/screen")), + 'copy_clipboard_cmd': ("xclip -selection c", + ("command used to copy to clipboard; must read " + "input from stdin")), + 'paste_clipboard_cmd': ("xclip -selection c -o", + ("command used to paste clipboard; must output " + "content to stdout")), + 'imap_esc': ("", ("use alternate mapping to enter Normal mode while in " + "Insert mode; having it set to 'jk' is similar to " + "`:imap jk ` in vim")), + 'imap_esc_timeout': ("1000", ("time in ms to wait for the imap_esc " + "sequence to complete")), + 'search_vim': ("off", ("allow n/N usage after searching (requires an extra" + " to return to normal mode)")), + 'user_mappings': ("", ("see the `:nmap` command in the README for more " + "info; please do not modify this field manually " + "unless you know what you're doing")), + 'mode_indicator_prefix': ("", "prefix for the bar item mode_indicator"), + 'mode_indicator_suffix': ("", "suffix for the bar item mode_indicator"), + 'mode_indicator_normal_color': ("white", + "color for mode indicator in Normal mode"), + 'mode_indicator_normal_color_bg': ("gray", + ("background color for mode indicator " + "in Normal mode")), + 'mode_indicator_insert_color': ("white", + "color for mode indicator in Insert mode"), + 'mode_indicator_insert_color_bg': ("blue", + ("background color for mode indicator " + "in Insert mode")), + 'mode_indicator_replace_color': ("white", + "color for mode indicator in Replace mode"), + 'mode_indicator_replace_color_bg': ("red", + ("background color for mode indicator " + "in Replace mode")), + 'mode_indicator_cmd_color': ("white", + "color for mode indicator in Command mode"), + 'mode_indicator_cmd_color_bg': ("cyan", + ("background color for mode indicator in " + "Command mode")), + 'mode_indicator_search_color': ("white", + "color for mode indicator in Search mode"), + 'mode_indicator_search_color_bg': ("magenta", + ("background color for mode indicator " + "in Search mode")), + 'line_number_prefix': ("", "prefix for line numbers"), + 'line_number_suffix': (" ", "suffix for line numbers") +} + + +# Regex patterns. +# --------------- + +WHITESPACE = re.compile(r"\s") +IS_KEYWORD = re.compile(r"[a-zA-Z0-9_@À-ÿ]") +REGEX_MOTION_LOWERCASE_W = re.compile(r"\b\S|(?<=\s)\S") +REGEX_MOTION_UPPERCASE_W = re.compile(r"(?<=\s)\S") +REGEX_MOTION_UPPERCASE_E = re.compile(r"\S(?!\S)") +REGEX_MOTION_UPPERCASE_B = REGEX_MOTION_UPPERCASE_E +REGEX_MOTION_G_UPPERCASE_E = REGEX_MOTION_UPPERCASE_W +REGEX_MOTION_CARRET = re.compile(r"\S") +REGEX_INT = r"[0-9]" +REGEX_MAP_KEYS_1 = { + re.compile("<([^>]*-)Left>", re.IGNORECASE): '<\\1\x01[[D>', + re.compile("<([^>]*-)Right>", re.IGNORECASE): '<\\1\x01[[C>', + re.compile("<([^>]*-)Up>", re.IGNORECASE): '<\\1\x01[[A>', + re.compile("<([^>]*-)Down>", re.IGNORECASE): '<\\1\x01[[B>', + re.compile("", re.IGNORECASE): '\x01[[D', + re.compile("", re.IGNORECASE): '\x01[[C', + re.compile("", re.IGNORECASE): '\x01[[A', + re.compile("", re.IGNORECASE): '\x01[[B' +} +REGEX_MAP_KEYS_2 = { + re.compile(r"]*)>", re.IGNORECASE): '\x01\\1', + re.compile(r"]*)>", re.IGNORECASE): '\x01[\\1' +} + +# Regex used to detect problematic keybindings. +# For example: meta-wmeta-s is bound by default to ``/window swap``. +# If the user pressed Esc-w, WeeChat will detect it as meta-w and will not +# send any signal to `cb_key_combo_default()` just yet, since it's the +# beginning of a known key combo. +# Instead, `cb_key_combo_default()` will receive the Esc-ws signal, which +# becomes "ws" after removing the Esc part, and won't know how to handle it. +REGEX_PROBLEMATIC_KEYBINDINGS = re.compile(r"meta-\w(meta|ctrl)") + + +# Vi commands. +# ------------ + +def cmd_nmap(args): + """Add a user-defined key mapping. + + Some (but not all) vim-like key codes are supported to simplify things for + the user: , , , , and . + + See Also: + `cmd_unmap()`. + """ + args = args.strip() + if not args: + mappings = vimode_settings['user_mappings'] + if mappings: + weechat.prnt("", "User-defined key mappings:") + for key, mapping in mappings.items(): + weechat.prnt("", "{} -> {}".format(key, mapping)) + else: + weechat.prnt("", "nmap: no mapping found.") + elif not " " in args: + weechat.prnt("", "nmap syntax -> :nmap {lhs} {rhs}") + else: + key, mapping = args.split(" ", 1) + # First pass of replacements. We perform two passes as a simple way to + # avoid incorrect replacements due to dictionaries not being + # insertion-ordered prior to Python 3.7. + for regex, repl in REGEX_MAP_KEYS_1.items(): + key = regex.sub(repl, key) + mapping = regex.sub(repl, mapping) + # Second pass of replacements. + for regex, repl in REGEX_MAP_KEYS_2.items(): + key = regex.sub(repl, key) + mapping = regex.sub(repl, mapping) + mappings = vimode_settings['user_mappings'] + mappings[key] = mapping + weechat.config_set_plugin('user_mappings', json.dumps(mappings)) + vimode_settings['user_mappings'] = mappings + +def cmd_nunmap(args): + """Remove a user-defined key mapping. + + See Also: + `cmd_map()`. + """ + args = args.strip() + if not args: + weechat.prnt("", "nunmap syntax -> :unmap {lhs}") + else: + key = args + for regex, repl in REGEX_MAP_KEYS_1.items(): + key = regex.sub(repl, key) + for regex, repl in REGEX_MAP_KEYS_2.items(): + key = regex.sub(repl, key) + mappings = vimode_settings['user_mappings'] + if key in mappings: + del mappings[key] + weechat.config_set_plugin('user_mappings', json.dumps(mappings)) + vimode_settings['user_mappings'] = mappings + else: + weechat.prnt("", "nunmap: No such mapping") + +# See Also: `cb_exec_cmd()`. +VI_COMMAND_GROUPS = {('h', 'help'): "/help", + ('qa', 'qall', 'quita', 'quitall'): "/exit", + ('q', 'quit'): "/close", + ('w', 'write'): "/save", + ('bN', 'bNext', 'bp', 'bprevious'): "/buffer -1", + ('bn', 'bnext'): "/buffer +1", + ('bd', 'bdel', 'bdelete'): "/close", + ('b#',): "/input jump_last_buffer_displayed", + ('b', 'bu', 'buf', 'buffer'): "/buffer", + ('sp', 'split'): "/window splith", + ('vs', 'vsplit'): "/window splitv", + ('nm', 'nmap'): cmd_nmap, + ('nun', 'nunmap'): cmd_nunmap} + +VI_COMMANDS = dict() +for T, v in VI_COMMAND_GROUPS.items(): + VI_COMMANDS.update(dict.fromkeys(T, v)) + + +# Vi operators. +# ------------- + +# Each operator must have a corresponding function, called "operator_X" where +# X is the operator. For example: `operator_c()`. +VI_OPERATORS = ["c", "d", "y"] + + +# Vi motions. +# ----------- + +# Vi motions. Each motion must have a corresponding function, called +# "motion_X" where X is the motion (e.g. `motion_w()`). +# See Also: `SPECIAL_CHARS`. +VI_MOTIONS = ["w", "e", "b", "^", "$", "h", "l", "W", "E", "B", "f", "F", "t", + "T", "ge", "gE", "0"] + +# Special characters for motions. The corresponding function's name is +# converted before calling. For example, "^" will call `motion_carret` instead +# of `motion_^` (which isn't allowed because of illegal characters). +SPECIAL_CHARS = {'^': "carret", + '$': "dollar"} + + +# Methods for vi operators, motions and key bindings. +# =================================================== + +# Documented base examples: +# ------------------------- + +def operator_base(buf, input_line, pos1, pos2, overwrite): + """Operator method example. + + Args: + buf (str): pointer to the current WeeChat buffer. + input_line (str): the content of the input line. + pos1 (int): the starting position of the motion. + pos2 (int): the ending position of the motion. + overwrite (bool, optional): whether the character at the cursor's new + position should be overwritten or not (for inclusive motions). + Defaults to False. + + Notes: + Should be called "operator_X", where X is the operator, and defined in + `VI_OPERATORS`. + Must perform actions (e.g. modifying the input line) on its own, + using the WeeChat API. + + See Also: + For additional examples, see `operator_d()` and + `operator_y()`. + """ + # Get start and end positions. + start = min(pos1, pos2) + end = max(pos1, pos2) + # Print the text the operator should go over. + weechat.prnt("", "Selection: %s" % input_line[start:end]) + +def motion_base(input_line, cur, count): + """Motion method example. + + Args: + input_line (str): the content of the input line. + cur (int): the position of the cursor. + count (int): the amount of times to multiply or iterate the action. + + Returns: + A tuple containing three values: + int: the new position of the cursor. + bool: True if the motion is inclusive, False otherwise. + bool: True if the motion is catching, False otherwise. + See `start_catching_keys()` for more info on catching motions. + + Notes: + Should be called "motion_X", where X is the motion, and defined in + `VI_MOTIONS`. + Must not modify the input line directly. + + See Also: + For additional examples, see `motion_w()` (normal motion) and + `motion_f()` (catching motion). + """ + # Find (relative to cur) position of next number. + pos = get_pos(input_line, REGEX_INT, cur, True, count) + # Return the new (absolute) cursor position. + # This motion is exclusive, so overwrite is False. + return cur + pos, False + +def key_base(buf, input_line, cur, count): + """Key method example. + + Args: + buf (str): pointer to the current WeeChat buffer. + input_line (str): the content of the input line. + cur (int): the position of the cursor. + count (int): the amount of times to multiply or iterate the action. + + Notes: + Should be called `key_X`, where X represents the key(s), and defined + in `VI_KEYS`. + Must perform actions on its own (using the WeeChat API). + + See Also: + For additional examples, see `key_a()` (normal key) and + `key_r()` (catching key). + """ + # Key was pressed. Go to Insert mode (similar to "i"). + set_mode("INSERT") + + +# Operators: +# ---------- + +def operator_d(buf, input_line, pos1, pos2, overwrite=False): + """Delete text from `pos1` to `pos2` from the input line. + + If `overwrite` is set to True, the character at the cursor's new position + is removed as well (the motion is inclusive). + + See Also: + `operator_base()`. + """ + start = min(pos1, pos2) + end = max(pos1, pos2) + if overwrite: + end += 1 + input_line = list(input_line) + del input_line[start:end] + input_line = "".join(input_line) + weechat.buffer_set(buf, "input", input_line) + set_cur(buf, input_line, pos1) + +def operator_c(buf, input_line, pos1, pos2, overwrite=False): + """Delete text from `pos1` to `pos2` from the input and enter Insert mode. + + If `overwrite` is set to True, the character at the cursor's new position + is removed as well (the motion is inclusive.) + + See Also: + `operator_base()`. + """ + operator_d(buf, input_line, pos1, pos2, overwrite) + set_mode("INSERT") + +def operator_y(buf, input_line, pos1, pos2, _): + """Yank text from `pos1` to `pos2` from the input line. + + See Also: + `operator_base()`. + """ + start = min(pos1, pos2) + end = max(pos1, pos2) + cmd = vimode_settings['copy_clipboard_cmd'] + proc = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE) + proc.communicate(input=input_line[start:end].encode()) + + +# Motions: +# -------- + +def motion_0(input_line, cur, count): + """Go to the first character of the line. + + See Also; + `motion_base()`. + """ + return 0, False, False + +def motion_w(input_line, cur, count): + """Go `count` words forward and return position. + + See Also: + `motion_base()`. + """ + pos = get_pos(input_line, REGEX_MOTION_LOWERCASE_W, cur, True, count) + if pos == -1: + return len(input_line), False, False + return cur + pos, False, False + +def motion_W(input_line, cur, count): + """Go `count` WORDS forward and return position. + + See Also: + `motion_base()`. + """ + pos = get_pos(input_line, REGEX_MOTION_UPPERCASE_W, cur, True, count) + if pos == -1: + return len(input_line), False, False + return cur + pos, False, False + +def motion_e(input_line, cur, count): + """Go to the end of `count` words and return position. + + See Also: + `motion_base()`. + """ + for _ in range(max(1, count)): + found = False + pos = cur + for pos in range(cur + 1, len(input_line) - 1): + # Whitespace, keep going. + if WHITESPACE.match(input_line[pos]): + pass + # End of sequence made from 'iskeyword' characters only, + # or end of sequence made from non 'iskeyword' characters only. + elif ((IS_KEYWORD.match(input_line[pos]) and + (not IS_KEYWORD.match(input_line[pos + 1]) or + WHITESPACE.match(input_line[pos + 1]))) or + (not IS_KEYWORD.match(input_line[pos]) and + (IS_KEYWORD.match(input_line[pos + 1]) or + WHITESPACE.match(input_line[pos + 1])))): + found = True + cur = pos + break + # We're at the character before the last and we still found nothing. + # Go to the last character. + if not found: + cur = pos + 1 + return cur, True, False + +def motion_E(input_line, cur, count): + """Go to the end of `count` WORDS and return cusor position. + + See Also: + `motion_base()`. + """ + pos = get_pos(input_line, REGEX_MOTION_UPPERCASE_E, cur, True, count) + if pos == -1: + return len(input_line), False, False + return cur + pos, True, False + +def motion_b(input_line, cur, count): + """Go `count` words backwards and return position. + + See Also: + `motion_base()`. + """ + # "b" is just "e" on inverted data (e.g. "olleH" instead of "Hello"). + pos_inv = motion_e(input_line[::-1], len(input_line) - cur - 1, count)[0] + pos = len(input_line) - pos_inv - 1 + return pos, True, False + +def motion_B(input_line, cur, count): + """Go `count` WORDS backwards and return position. + + See Also: + `motion_base()`. + """ + new_cur = len(input_line) - cur + pos = get_pos(input_line[::-1], REGEX_MOTION_UPPERCASE_B, new_cur, + count=count) + if pos == -1: + return 0, False, False + pos = len(input_line) - (pos + new_cur + 1) + return pos, True, False + +def motion_ge(input_line, cur, count): + """Go to end of `count` words backwards and return position. + + See Also: + `motion_base()`. + """ + # "ge is just "w" on inverted data (e.g. "olleH" instead of "Hello"). + pos_inv = motion_w(input_line[::-1], len(input_line) - cur - 1, count)[0] + pos = len(input_line) - pos_inv - 1 + return pos, True, False + +def motion_gE(input_line, cur, count): + """Go to end of `count` WORDS backwards and return position. + + See Also: + `motion_base()`. + """ + new_cur = len(input_line) - cur - 1 + pos = get_pos(input_line[::-1], REGEX_MOTION_G_UPPERCASE_E, new_cur, + True, count) + if pos == -1: + return 0, False, False + pos = len(input_line) - (pos + new_cur + 1) + return pos, True, False + +def motion_h(input_line, cur, count): + """Go `count` characters to the left and return position. + + See Also: + `motion_base()`. + """ + return max(0, cur - max(count, 1)), False, False + +def motion_l(input_line, cur, count): + """Go `count` characters to the right and return position. + + See Also: + `motion_base()`. + """ + return cur + max(count, 1), False, False + +def motion_carret(input_line, cur, count): + """Go to first non-blank character of line and return position. + + See Also: + `motion_base()`. + """ + pos = get_pos(input_line, REGEX_MOTION_CARRET, 0) + return pos, False, False + +def motion_dollar(input_line, cur, count): + """Go to end of line and return position. + + See Also: + `motion_base()`. + """ + pos = len(input_line) + return pos, False, False + +def motion_f(input_line, cur, count): + """Go to `count`'th occurence of character and return position. + + See Also: + `motion_base()`. + """ + return start_catching_keys(1, "cb_motion_f", input_line, cur, count) + +def cb_motion_f(update_last=True): + """Callback for `motion_f()`. + + Args: + update_last (bool, optional): should `last_search_motion` be updated? + Set to False when calling from `key_semicolon()` or `key_comma()` + so that the last search motion isn't overwritten. + Defaults to True. + + See Also: + `start_catching_keys()`. + """ + global last_search_motion + pattern = catching_keys_data['keys'] + pos = get_pos(catching_keys_data['input_line'], re.escape(pattern), + catching_keys_data['cur'], True, + catching_keys_data['count']) + catching_keys_data['new_cur'] = max(0, pos) + catching_keys_data['cur'] + if update_last: + last_search_motion = {'motion': "f", 'data': pattern} + cb_key_combo_default(None, None, "") + +def motion_F(input_line, cur, count): + """Go to `count`'th occurence of char to the right and return position. + + See Also: + `motion_base()`. + """ + return start_catching_keys(1, "cb_motion_F", input_line, cur, count) + +def cb_motion_F(update_last=True): + """Callback for `motion_F()`. + + Args: + update_last (bool, optional): should `last_search_motion` be updated? + Set to False when calling from `key_semicolon()` or `key_comma()` + so that the last search motion isn't overwritten. + Defaults to True. + + See Also: + `start_catching_keys()`. + """ + global last_search_motion + pattern = catching_keys_data['keys'] + cur = len(catching_keys_data['input_line']) - catching_keys_data['cur'] + pos = get_pos(catching_keys_data['input_line'][::-1], + re.escape(pattern), + cur, + False, + catching_keys_data['count']) + catching_keys_data['new_cur'] = catching_keys_data['cur'] - max(0, pos + 1) + if update_last: + last_search_motion = {'motion': "F", 'data': pattern} + cb_key_combo_default(None, None, "") + +def motion_t(input_line, cur, count): + """Go to `count`'th occurence of char and return position. + + The position returned is the position of the character to the left of char. + + See Also: + `motion_base()`. + """ + return start_catching_keys(1, "cb_motion_t", input_line, cur, count) + +def cb_motion_t(update_last=True): + """Callback for `motion_t()`. + + Args: + update_last (bool, optional): should `last_search_motion` be updated? + Set to False when calling from `key_semicolon()` or `key_comma()` + so that the last search motion isn't overwritten. + Defaults to True. + + See Also: + `start_catching_keys()`. + """ + global last_search_motion + pattern = catching_keys_data['keys'] + pos = get_pos(catching_keys_data['input_line'], re.escape(pattern), + catching_keys_data['cur'] + 1, + True, catching_keys_data['count']) + pos += 1 + if pos > 0: + catching_keys_data['new_cur'] = pos + catching_keys_data['cur'] - 1 + else: + catching_keys_data['new_cur'] = catching_keys_data['cur'] + if update_last: + last_search_motion = {'motion': "t", 'data': pattern} + cb_key_combo_default(None, None, "") + +def motion_T(input_line, cur, count): + """Go to `count`'th occurence of char to the left and return position. + + The position returned is the position of the character to the right of + char. + + See Also: + `motion_base()`. + """ + return start_catching_keys(1, "cb_motion_T", input_line, cur, count) + +def cb_motion_T(update_last=True): + """Callback for `motion_T()`. + + Args: + update_last (bool, optional): should `last_search_motion` be updated? + Set to False when calling from `key_semicolon()` or `key_comma()` + so that the last search motion isn't overwritten. + Defaults to True. + + See Also: + `start_catching_keys()`. + """ + global last_search_motion + pattern = catching_keys_data['keys'] + pos = get_pos(catching_keys_data['input_line'][::-1], re.escape(pattern), + (len(catching_keys_data['input_line']) - + (catching_keys_data['cur'] + 1)) + 1, + True, catching_keys_data['count']) + pos += 1 + if pos > 0: + catching_keys_data['new_cur'] = catching_keys_data['cur'] - pos + 1 + else: + catching_keys_data['new_cur'] = catching_keys_data['cur'] + if update_last: + last_search_motion = {'motion': "T", 'data': pattern} + cb_key_combo_default(None, None, "") + + +# Keys: +# ----- + +def key_cc(buf, input_line, cur, count): + """Delete line and start Insert mode. + + See Also: + `key_base()`. + """ + weechat.command("", "/input delete_line") + set_mode("INSERT") + +def key_C(buf, input_line, cur, count): + """Delete from cursor to end of line and start Insert mode. + + See Also: + `key_base()`. + """ + weechat.command("", "/input delete_end_of_line") + set_mode("INSERT") + +def key_yy(buf, input_line, cur, count): + """Yank line. + + See Also: + `key_base()`. + """ + cmd = vimode_settings['copy_clipboard_cmd'] + proc = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE) + proc.communicate(input=input_line.encode()) + +def key_p(buf, input_line, cur, count): + """Paste text. + + See Also: + `key_base()`. + """ + cmd = vimode_settings['paste_clipboard_cmd'] + weechat.hook_process(cmd, 10 * 1000, "cb_key_p", weechat.current_buffer()) + +def cb_key_p(data, command, return_code, output, err): + """Callback for fetching clipboard text and pasting it.""" + buf = "" + this_buffer = data + if output != "": + buf += output.strip() + if return_code == 0: + my_input = weechat.buffer_get_string(this_buffer, "input") + pos = weechat.buffer_get_integer(this_buffer, "input_pos") + my_input = my_input[:pos] + buf + my_input[pos:] + pos += len(buf) + weechat.buffer_set(this_buffer, "input", my_input) + weechat.buffer_set(this_buffer, "input_pos", str(pos)) + return weechat.WEECHAT_RC_OK + +def key_i(buf, input_line, cur, count): + """Start Insert mode. + + See Also: + `key_base()`. + """ + set_mode("INSERT") + +def key_a(buf, input_line, cur, count): + """Move cursor one character to the right and start Insert mode. + + See Also: + `key_base()`. + """ + set_cur(buf, input_line, cur + 1, False) + set_mode("INSERT") + +def key_A(buf, input_line, cur, count): + """Move cursor to end of line and start Insert mode. + + See Also: + `key_base()`. + """ + set_cur(buf, input_line, len(input_line), False) + set_mode("INSERT") + +def key_I(buf, input_line, cur, count): + """Move cursor to first non-blank character and start Insert mode. + + See Also: + `key_base()`. + """ + pos, _, _ = motion_carret(input_line, cur, 0) + set_cur(buf, input_line, pos) + set_mode("INSERT") + +def key_G(buf, input_line, cur, count): + """Scroll to specified line or bottom of buffer. + + See Also: + `key_base()`. + """ + if count > 0: + # This is necessary to prevent weird scroll jumps. + weechat.command("", "/window scroll_top") + weechat.command("", "/window scroll %s" % (count - 1)) + else: + weechat.command("", "/window scroll_bottom") + +def key_r(buf, input_line, cur, count): + """Replace `count` characters under the cursor. + + See Also: + `key_base()`. + """ + start_catching_keys(1, "cb_key_r", input_line, cur, count, buf) + +def cb_key_r(): + """Callback for `key_r()`. + + See Also: + `start_catching_keys()`. + """ + global catching_keys_data + input_line = list(catching_keys_data['input_line']) + count = max(catching_keys_data['count'], 1) + cur = catching_keys_data['cur'] + if cur + count <= len(input_line): + for _ in range(count): + input_line[cur] = catching_keys_data['keys'] + cur += 1 + input_line = "".join(input_line) + weechat.buffer_set(catching_keys_data['buf'], "input", input_line) + set_cur(catching_keys_data['buf'], input_line, cur - 1) + catching_keys_data = {'amount': 0} + +def key_R(buf, input_line, cur, count): + """Start Replace mode. + + See Also: + `key_base()`. + """ + set_mode("REPLACE") + +def key_tilda(buf, input_line, cur, count): + """Switch the case of `count` characters under the cursor. + + See Also: + `key_base()`. + """ + input_line = list(input_line) + count = max(1, count) + while count and cur < len(input_line): + input_line[cur] = input_line[cur].swapcase() + count -= 1 + cur += 1 + input_line = "".join(input_line) + weechat.buffer_set(buf, "input", input_line) + set_cur(buf, input_line, cur) + +def key_alt_j(buf, input_line, cur, count): + """Go to WeeChat buffer. + + Called to preserve WeeChat's alt-j buffer switching. + + This is only called when alt-j is pressed after pressing Esc, because + \x01\x01j is received in key_combo_default which becomes \x01j after + removing the detected Esc key. + If Esc isn't the last pressed key, \x01j is directly received in + key_combo_default. + """ + start_catching_keys(2, "cb_key_alt_j", input_line, cur, count) + +def cb_key_alt_j(): + """Callback for `key_alt_j()`. + + See Also: + `start_catching_keys()`. + """ + global catching_keys_data + weechat.command("", "/buffer " + catching_keys_data['keys']) + catching_keys_data = {'amount': 0} + +def key_semicolon(buf, input_line, cur, count, swap=False): + """Repeat last f, t, F, T `count` times. + + Args: + swap (bool, optional): if True, the last motion will be repeated in the + opposite direction (e.g. "f" instead of "F"). Defaults to False. + + See Also: + `key_base()`. + """ + global catching_keys_data, vi_buffer + catching_keys_data = ({'amount': 0, + 'input_line': input_line, + 'cur': cur, + 'keys': last_search_motion['data'], + 'count': count, + 'new_cur': 0, + 'buf': buf}) + # Swap the motion's case if called from key_comma. + if swap: + motion = last_search_motion['motion'].swapcase() + else: + motion = last_search_motion['motion'] + func = "cb_motion_%s" % motion + vi_buffer = motion + globals()[func](False) + +def key_comma(buf, input_line, cur, count): + """Repeat last f, t, F, T in opposite direction `count` times. + + See Also: + `key_base()`. + """ + key_semicolon(buf, input_line, cur, count, True) + +def key_u(buf, input_line, cur, count): + """Undo change `count` times. + + See Also: + `key_base()`. + """ + buf = weechat.current_buffer() + if buf not in undo_history: + return + for _ in range(max(count, 1)): + if undo_history_index[buf] > -len(undo_history[buf]): + undo_history_index[buf] -= 1 + input_line = undo_history[buf][undo_history_index[buf]] + weechat.buffer_set(buf, "input", input_line) + else: + break + +def key_ctrl_r(buf, input_line, cur, count): + """Redo change `count` times. + + See Also: + `key_base()`. + """ + if buf not in undo_history: + return + for _ in range(max(count, 1)): + if undo_history_index[buf] < -1: + undo_history_index[buf] += 1 + input_line = undo_history[buf][undo_history_index[buf]] + weechat.buffer_set(buf, "input", input_line) + else: + break + + +# Vi key bindings. +# ================ + +# String values will be executed as normal WeeChat commands. +# For functions, see `key_base()` for reference. +VI_KEYS = {'j': "/window scroll_down", + 'k': "/window scroll_up", + 'G': key_G, + 'gg': "/window scroll_top", + 'x': "/input delete_next_char", + 'X': "/input delete_previous_char", + 'dd': "/input delete_line", + 'D': "/input delete_end_of_line", + 'cc': key_cc, + 'C': key_C, + 'i': key_i, + 'a': key_a, + 'A': key_A, + 'I': key_I, + 'yy': key_yy, + 'p': key_p, + 'gt': "/buffer -1", + 'K': "/buffer -1", + 'gT': "/buffer +1", + 'J': "/buffer +1", + 'r': key_r, + 'R': key_R, + '~': key_tilda, + 'nt': "/bar scroll nicklist * -100%", + 'nT': "/bar scroll nicklist * +100%", + '\x01[[A': "/input history_previous", + '\x01[[B': "/input history_next", + '\x01[[C': "/input move_next_char", + '\x01[[D': "/input move_previous_char", + '\x01[[H': "/input move_beginning_of_line", + '\x01[[F': "/input move_end_of_line", + '\x01[[5~': "/window page_up", + '\x01[[6~': "/window page_down", + '\x01[[3~': "/input delete_next_char", + '\x01[[2~': key_i, + '\x01M': "/input return", + '\x01?': "/input move_previous_char", + ' ': "/input move_next_char", + '\x01[j': key_alt_j, + '\x01[1': "/buffer *1", + '\x01[2': "/buffer *2", + '\x01[3': "/buffer *3", + '\x01[4': "/buffer *4", + '\x01[5': "/buffer *5", + '\x01[6': "/buffer *6", + '\x01[7': "/buffer *7", + '\x01[8': "/buffer *8", + '\x01[9': "/buffer *9", + '\x01[0': "/buffer *10", + '\x01^': "/input jump_last_buffer_displayed", + '\x01D': "/window page_down", + '\x01U': "/window page_up", + '\x01Wh': "/window left", + '\x01Wj': "/window down", + '\x01Wk': "/window up", + '\x01Wl': "/window right", + '\x01W=': "/window balance", + '\x01Wx': "/window swap", + '\x01Ws': "/window splith", + '\x01Wv': "/window splitv", + '\x01Wq': "/window merge", + ';': key_semicolon, + ',': key_comma, + 'u': key_u, + '\x01R': key_ctrl_r} + +# Add alt-j bindings. +for i in range(10, 99): + VI_KEYS['\x01[j%s' % i] = "/buffer %s" % i + + +# Key handling. +# ============= + +def cb_key_pressed(data, signal, signal_data): + """Detect potential Esc presses. + + Alt and Esc are detected as the same key in most terminals. The difference + is that Alt signal is sent just before the other pressed key's signal. + We therefore use a timeout (50ms) to detect whether Alt or Esc was pressed. + """ + global last_signal_time + last_signal_time = time.time() + if signal_data == "\x01[": + # In 50ms, check if any other keys were pressed. If not, it's Esc! + weechat.hook_timer(50, 0, 1, "cb_check_esc", + "{:f}".format(last_signal_time)) + return weechat.WEECHAT_RC_OK + +def cb_check_esc(data, remaining_calls): + """Check if the Esc key was pressed and change the mode accordingly.""" + global esc_pressed, vi_buffer, catching_keys_data + # Not perfect, would be better to use direct comparison (==) but that only + # works for py2 and not for py3. + if abs(last_signal_time - float(data)) <= 0.000001: + esc_pressed += 1 + if mode == "SEARCH": + weechat.command("", "/input search_stop_here") + set_mode("NORMAL") + # Cancel any current partial commands. + vi_buffer = "" + catching_keys_data = {'amount': 0} + weechat.bar_item_update("vi_buffer") + return weechat.WEECHAT_RC_OK + +def cb_key_combo_default(data, signal, signal_data): + """Eat and handle key events when in Normal mode, if needed. + + The key_combo_default signal is sent when a key combo is pressed. For + example, alt-k will send the "\x01[k" signal. + + Esc is handled a bit differently to avoid delays, see `cb_key_pressed()`. + """ + global esc_pressed, vi_buffer, cmd_compl_text, cmd_text_orig, \ + cmd_compl_pos, cmd_history_index + + # If Esc was pressed, strip the Esc part from the pressed keys. + # Example: user presses Esc followed by i. This is detected as "\x01[i", + # but we only want to handle "i". + keys = signal_data + if esc_pressed or esc_pressed == -2: + if keys.startswith("\x01[" * esc_pressed): + # Multiples of 3 seem to "cancel" themselves, + # e.g. Esc-Esc-Esc-Alt-j-11 is detected as "\x01[\x01[\x01" + # followed by "\x01[j11" (two different signals). + if signal_data == "\x01[" * 3: + esc_pressed = -1 # `cb_check_esc()` will increment it to 0. + else: + esc_pressed = 0 + # This can happen if a valid combination is started but interrupted + # with Esc, such as Ctrl-W→Esc→w which would send two signals: + # "\x01W\x01[" then "\x01W\x01[w". + # In that case, we still need to handle the next signal ("\x01W\x01[w") + # so we use the special value "-2". + else: + esc_pressed = -2 + keys = keys.split("\x01[")[-1] # Remove the "Esc" part(s). + # Ctrl-Space. + elif keys == "\x01@": + set_mode("NORMAL") + return weechat.WEECHAT_RC_OK_EAT + + # Clear the undo history for this buffer on . + if keys == "\x01M": + buf = weechat.current_buffer() + clear_undo_history(buf) + + # Detect imap_esc presses if any. + if mode == "INSERT": + imap_esc = vimode_settings['imap_esc'] + if not imap_esc: + return weechat.WEECHAT_RC_OK + if (imap_esc.startswith(vi_buffer) and + imap_esc[len(vi_buffer):len(vi_buffer)+1] == keys): + vi_buffer += keys + weechat.bar_item_update("vi_buffer") + weechat.hook_timer(int(vimode_settings['imap_esc_timeout']), 0, 1, + "cb_check_imap_esc", vi_buffer) + elif (vi_buffer and imap_esc.startswith(vi_buffer) and + imap_esc[len(vi_buffer):len(vi_buffer)+1] != keys): + vi_buffer = "" + weechat.bar_item_update("vi_buffer") + # imap_esc sequence detected -- remove the sequence keys from the + # Weechat input bar and enter Normal mode. + if imap_esc == vi_buffer: + buf = weechat.current_buffer() + input_line = weechat.buffer_get_string(buf, "input") + cur = weechat.buffer_get_integer(buf, "input_pos") + input_line = (input_line[:cur-len(imap_esc)+1] + + input_line[cur:]) + weechat.buffer_set(buf, "input", input_line) + set_cur(buf, input_line, cur-len(imap_esc)+1, False) + set_mode("NORMAL") + vi_buffer = "" + weechat.bar_item_update("vi_buffer") + return weechat.WEECHAT_RC_OK_EAT + return weechat.WEECHAT_RC_OK + + # We're in Replace mode — allow "normal" key presses (e.g. "a") and + # overwrite the next character with them, but let the other key presses + # pass normally (e.g. backspace, arrow keys, etc). + if mode == "REPLACE": + if len(keys) == 1: + weechat.command("", "/input delete_next_char") + elif keys == "\x01?": + weechat.command("", "/input move_previous_char") + return weechat.WEECHAT_RC_OK_EAT + return weechat.WEECHAT_RC_OK + + # We're catching keys! Only "normal" key presses interest us (e.g. "a"), + # not complex ones (e.g. backspace). + if len(keys) == 1 and catching_keys_data['amount']: + catching_keys_data['keys'] += keys + catching_keys_data['amount'] -= 1 + # Done catching keys, execute the callback. + if catching_keys_data['amount'] == 0: + globals()[catching_keys_data['callback']]() + vi_buffer = "" + weechat.bar_item_update("vi_buffer") + return weechat.WEECHAT_RC_OK_EAT + + # We're in command-line mode. + if mode == "COMMAND": + buf = weechat.current_buffer() + cmd_text = weechat.buffer_get_string(buf, "input") + weechat.hook_timer(1, 0, 1, "cb_check_cmd_mode", "") + # Return key. + if keys == "\x01M": + weechat.hook_timer(1, 0, 1, "cb_exec_cmd", cmd_text) + if len(cmd_text) > 1 and (not cmd_history or + cmd_history[-1] != cmd_text): + cmd_history.append(cmd_text) + cmd_history_index = 0 + set_mode("NORMAL") + buf = weechat.current_buffer() + input_line = input_line_backup[buf]['input_line'] + weechat.buffer_set(buf, "input", input_line) + set_cur(buf, input_line, input_line_backup[buf]['cur'], False) + # Up arrow. + elif keys == "\x01[[A": + if cmd_history_index > -len(cmd_history): + cmd_history_index -= 1 + cmd_text = cmd_history[cmd_history_index] + weechat.buffer_set(buf, "input", cmd_text) + set_cur(buf, cmd_text, len(cmd_text), False) + # Down arrow. + elif keys == "\x01[[B": + if cmd_history_index < -1: + cmd_history_index += 1 + cmd_text = cmd_history[cmd_history_index] + else: + cmd_history_index = 0 + cmd_text = ":" + weechat.buffer_set(buf, "input", cmd_text) + set_cur(buf, cmd_text, len(cmd_text), False) + # Tab key. No completion when searching ("/"). + elif keys == "\x01I" and cmd_text[0] == ":": + if cmd_text_orig is None: + input_ = list(cmd_text) + del input_[0] + cmd_text_orig = "".join(input_) + cmd_compl_list = [] + for cmd in VI_COMMANDS.keys(): + if cmd.startswith(cmd_text_orig): + cmd_compl_list.append(cmd) + if cmd_compl_list: + curr_suggestion = cmd_compl_list[cmd_compl_pos] + cmd_text = ":%s" % curr_suggestion + cmd_compl_list[cmd_compl_pos] = weechat.string_eval_expression( + "${color:bold}%s${color:-bold}" % curr_suggestion, + {}, {}, {}) + cmd_compl_text = ", ".join(cmd_compl_list) + cmd_compl_pos = (cmd_compl_pos + 1) % len(cmd_compl_list) + weechat.buffer_set(buf, "input", cmd_text) + set_cur(buf, cmd_text, len(cmd_text), False) + # Input. + else: + cmd_compl_text = "" + cmd_text_orig = None + cmd_compl_pos = 0 + weechat.bar_item_update("cmd_completion") + if keys in ["\x01M", "\x01[[A", "\x01[[B"]: + cmd_compl_text = "" + return weechat.WEECHAT_RC_OK_EAT + else: + return weechat.WEECHAT_RC_OK + # Enter command mode. + elif keys in [":", "/"]: + if keys == "/": + weechat.command("", "/input search_text_here") + if not weechat.config_string_to_boolean( + vimode_settings['search_vim']): + return weechat.WEECHAT_RC_OK + else: + buf = weechat.current_buffer() + cur = weechat.buffer_get_integer(buf, "input_pos") + input_line = weechat.buffer_get_string(buf, "input") + input_line_backup[buf] = {'input_line': input_line, 'cur': cur} + input_line = ":" + weechat.buffer_set(buf, "input", input_line) + set_cur(buf, input_line, 1, False) + set_mode("COMMAND") + cmd_compl_text = "" + cmd_text_orig = None + cmd_compl_pos = 0 + return weechat.WEECHAT_RC_OK_EAT + + # Add key to the buffer. + vi_buffer += keys + weechat.bar_item_update("vi_buffer") + if not vi_buffer: + return weechat.WEECHAT_RC_OK + + # Check if the keys have a (partial or full) match. If so, also get the + # keys without the count. (These are the actual keys we should handle.) + # After that, `vi_buffer` is only used for display purposes — only + # `vi_keys` is checked for all the handling. + # If no matches are found, the keys buffer is cleared. + matched, vi_keys, count = get_keys_and_count(vi_buffer) + if not matched: + vi_buffer = "" + return weechat.WEECHAT_RC_OK_EAT + # Check if it's a command (user defined key mapped to a :cmd). + if vi_keys.startswith(":"): + weechat.hook_timer(1, 0, 1, "cb_exec_cmd", "{} {}".format(vi_keys, + count)) + vi_buffer = "" + return weechat.WEECHAT_RC_OK_EAT + # It's a WeeChat command (user defined key mapped to a /cmd). + if vi_keys.startswith("/"): + weechat.command("", vi_keys) + vi_buffer = "" + return weechat.WEECHAT_RC_OK_EAT + + buf = weechat.current_buffer() + input_line = weechat.buffer_get_string(buf, "input") + cur = weechat.buffer_get_integer(buf, "input_pos") + + # It's a default mapping. If the corresponding value is a string, we assume + # it's a WeeChat command. Otherwise, it's a method we'll call. + if vi_keys in VI_KEYS: + if vi_keys not in ['u', '\x01R']: + add_undo_history(buf, input_line) + if isinstance(VI_KEYS[vi_keys], str): + for _ in range(max(count, 1)): + # This is to avoid crashing WeeChat on script reloads/unloads, + # because no hooks must still be running when a script is + # reloaded or unloaded. + if (VI_KEYS[vi_keys] == "/input return" and + input_line.startswith("/script ")): + return weechat.WEECHAT_RC_OK + weechat.command("", VI_KEYS[vi_keys]) + current_cur = weechat.buffer_get_integer(buf, "input_pos") + set_cur(buf, input_line, current_cur) + else: + VI_KEYS[vi_keys](buf, input_line, cur, count) + # It's a motion (e.g. "w") — call `motion_X()` where X is the motion, then + # set the cursor's position to what that function returned. + elif vi_keys in VI_MOTIONS: + if vi_keys in SPECIAL_CHARS: + func = "motion_%s" % SPECIAL_CHARS[vi_keys] + else: + func = "motion_%s" % vi_keys + end, _, _ = globals()[func](input_line, cur, count) + set_cur(buf, input_line, end) + # It's an operator + motion (e.g. "dw") — call `motion_X()` (where X is + # the motion), then we call `operator_Y()` (where Y is the operator) + # with the position `motion_X()` returned. `operator_Y()` should then + # handle changing the input line. + elif (len(vi_keys) > 1 and + vi_keys[0] in VI_OPERATORS and + vi_keys[1:] in VI_MOTIONS): + add_undo_history(buf, input_line) + if vi_keys[1:] in SPECIAL_CHARS: + func = "motion_%s" % SPECIAL_CHARS[vi_keys[1:]] + else: + func = "motion_%s" % vi_keys[1:] + pos, overwrite, catching = globals()[func](input_line, cur, count) + # If it's a catching motion, we don't want to call the operator just + # yet -- this code will run again when the motion is complete, at which + # point we will. + if not catching: + oper = "operator_%s" % vi_keys[0] + globals()[oper](buf, input_line, cur, pos, overwrite) + # The combo isn't completed yet (e.g. just "d"). + else: + return weechat.WEECHAT_RC_OK_EAT + + # We've already handled the key combo, so clear the keys buffer. + if not catching_keys_data['amount']: + vi_buffer = "" + weechat.bar_item_update("vi_buffer") + return weechat.WEECHAT_RC_OK_EAT + +def cb_check_imap_esc(data, remaining_calls): + """Clear the imap_esc sequence after some time if nothing was pressed.""" + global vi_buffer + if vi_buffer == data: + vi_buffer = "" + weechat.bar_item_update("vi_buffer") + return weechat.WEECHAT_RC_OK + +def cb_key_combo_search(data, signal, signal_data): + """Handle keys while search mode is active (if search_vim is enabled).""" + if not weechat.config_string_to_boolean(vimode_settings['search_vim']): + return weechat.WEECHAT_RC_OK + if mode == "COMMAND": + if signal_data == "\x01M": + set_mode("SEARCH") + return weechat.WEECHAT_RC_OK_EAT + elif mode == "SEARCH": + if signal_data == "\x01M": + set_mode("NORMAL") + else: + if signal_data == "n": + weechat.command("", "/input search_next") + elif signal_data == "N": + weechat.command("", "/input search_previous") + # Start a new search. + elif signal_data == "/": + weechat.command("", "/input search_stop_here") + set_mode("NORMAL") + weechat.command("", "/input search_text_here") + return weechat.WEECHAT_RC_OK_EAT + return weechat.WEECHAT_RC_OK + +# Callbacks. +# ========== + +# Bar items. +# ---------- + +def cb_vi_buffer(data, item, window): + """Return the content of the vi buffer (pressed keys on hold).""" + return vi_buffer + +def cb_cmd_completion(data, item, window): + """Return the text of the command line.""" + return cmd_compl_text + +def cb_mode_indicator(data, item, window): + """Return the current mode (INSERT/NORMAL/REPLACE/...).""" + return "{}{}{}{}{}".format( + weechat.color(mode_colors[mode]), + vimode_settings['mode_indicator_prefix'], mode, + vimode_settings['mode_indicator_suffix'], weechat.color("reset")) + +def cb_line_numbers(data, item, window): + """Fill the line numbers bar item.""" + bar_height = weechat.window_get_integer(window, "win_chat_height") + content = "" + for i in range(1, bar_height + 1): + content += "{}{}{}\n".format(vimode_settings['line_number_prefix'], i, + vimode_settings['line_number_suffix']) + return content + +# Callbacks for the line numbers bar. +# ................................... + +def cb_update_line_numbers(data, signal, signal_data): + """Call `cb_timer_update_line_numbers()` when switching buffers. + + A timer is required because the bar item is refreshed before the new buffer + is actually displayed, so ``win_chat_height`` would refer to the old + buffer. Using a timer refreshes the item after the new buffer is displayed. + """ + weechat.hook_timer(10, 0, 1, "cb_timer_update_line_numbers", "") + return weechat.WEECHAT_RC_OK + +def cb_timer_update_line_numbers(data, remaining_calls): + """Update the line numbers bar item.""" + weechat.bar_item_update("line_numbers") + return weechat.WEECHAT_RC_OK + + +# Config. +# ------- + +def cb_config(data, option, value): + """Script option changed, update our copy.""" + option_name = option.split(".")[-1] + if option_name in vimode_settings: + vimode_settings[option_name] = value + if option_name == 'user_mappings': + load_user_mappings() + if "_color" in option_name: + load_mode_colors() + return weechat.WEECHAT_RC_OK + +def load_mode_colors(): + mode_colors.update({ + 'NORMAL': "{},{}".format( + vimode_settings['mode_indicator_normal_color'], + vimode_settings['mode_indicator_normal_color_bg']), + 'INSERT': "{},{}".format( + vimode_settings['mode_indicator_insert_color'], + vimode_settings['mode_indicator_insert_color_bg']), + 'REPLACE': "{},{}".format( + vimode_settings['mode_indicator_replace_color'], + vimode_settings['mode_indicator_replace_color_bg']), + 'COMMAND': "{},{}".format( + vimode_settings['mode_indicator_cmd_color'], + vimode_settings['mode_indicator_cmd_color_bg']), + 'SEARCH': "{},{}".format( + vimode_settings['mode_indicator_search_color'], + vimode_settings['mode_indicator_search_color_bg']) + }) + +def load_user_mappings(): + """Load user-defined mappings.""" + mappings = {} + if vimode_settings['user_mappings']: + mappings.update(json.loads(vimode_settings['user_mappings'])) + vimode_settings['user_mappings'] = mappings + + +# Command-line execution. +# ----------------------- + +def cb_exec_cmd(data, remaining_calls): + """Translate and execute our custom commands to WeeChat command.""" + # Process the entered command. + data = list(data) + del data[0] + data = "".join(data) + # s/foo/bar command. + if data.startswith("s/"): + cmd = data + parsed_cmd = next(csv.reader(StringIO(cmd), delimiter="/", + escapechar="\\")) + pattern = re.escape(parsed_cmd[1]) + repl = parsed_cmd[2] + repl = re.sub(r"([^\\])&", r"\1" + pattern, repl) + flag = None + if len(parsed_cmd) == 4: + flag = parsed_cmd[3] + count = 1 + if flag == "g": + count = 0 + buf = weechat.current_buffer() + input_line = weechat.buffer_get_string(buf, "input") + input_line = re.sub(pattern, repl, input_line, count) + weechat.buffer_set(buf, "input", input_line) + # Shell command. + elif data.startswith("!"): + weechat.command("", "/exec -buffer shell %s" % data[1:]) + # Commands like `:22`. This should start cursor mode (``/cursor``) and take + # us to the relevant line. + elif data.isdigit(): + line_number = int(data) + hdata_window = weechat.hdata_get("window") + window = weechat.current_window() + x = weechat.hdata_integer(hdata_window, window, "win_chat_x") + y = (weechat.hdata_integer(hdata_window, window, "win_chat_y") + + (line_number - 1)) + weechat.command("", "/cursor go {},{}".format(x, y)) + # Check againt defined commands. + elif data: + raw_data = data + data = data.split(" ", 1) + cmd = data[0] + args = "" + if len(data) == 2: + args = data[1] + if cmd in VI_COMMANDS: + if isinstance(VI_COMMANDS[cmd], str): + weechat.command("", "%s %s" % (VI_COMMANDS[cmd], args)) + else: + VI_COMMANDS[cmd](args) + else: + # Check for commands not sepearated by space (e.g. "b2") + for i in range(1, len(raw_data)): + tmp_cmd = raw_data[:i] + tmp_args = raw_data[i:] + if tmp_cmd in VI_COMMANDS and tmp_args.isdigit(): + weechat.command("", "%s %s" % (VI_COMMANDS[tmp_cmd], + tmp_args)) + return weechat.WEECHAT_RC_OK + # No vi commands found, run the command as WeeChat command + weechat.command("", "/{} {}".format(cmd, args)) + return weechat.WEECHAT_RC_OK + +def cb_vimode_go_to_normal(data, buf, args): + set_mode("NORMAL") + return weechat.WEECHAT_RC_OK + +# Script commands. +# ---------------- + +def cb_vimode_cmd(data, buf, args): + """Handle script commands (``/vimode ``).""" + # ``/vimode`` or ``/vimode help`` + if not args or args == "help": + weechat.prnt("", "[vimode.py] %s" % README_URL) + # ``/vimode bind_keys`` or ``/vimode bind_keys --list`` + elif args.startswith("bind_keys"): + infolist = weechat.infolist_get("key", "", "default") + weechat.infolist_reset_item_cursor(infolist) + commands = ["/key unbind ctrl-W", + "/key bind ctrl-W /input delete_previous_word", + "/key bind ctrl-^ /input jump_last_buffer_displayed", + "/key bind ctrl-Wh /window left", + "/key bind ctrl-Wj /window down", + "/key bind ctrl-Wk /window up", + "/key bind ctrl-Wl /window right", + "/key bind ctrl-W= /window balance", + "/key bind ctrl-Wx /window swap", + "/key bind ctrl-Ws /window splith", + "/key bind ctrl-Wv /window splitv", + "/key bind ctrl-Wq /window merge"] + while weechat.infolist_next(infolist): + key = weechat.infolist_string(infolist, "key") + if re.match(REGEX_PROBLEMATIC_KEYBINDINGS, key): + commands.append("/key unbind %s" % key) + weechat.infolist_free(infolist) + if args == "bind_keys": + weechat.prnt("", "Running commands:") + for command in commands: + weechat.command("", command) + weechat.prnt("", "Done.") + elif args == "bind_keys --list": + weechat.prnt("", "Listing commands we'll run:") + for command in commands: + weechat.prnt("", " %s" % command) + weechat.prnt("", "Done.") + return weechat.WEECHAT_RC_OK + + +# Helpers. +# ======== + +# Motions/keys helpers. +# --------------------- + +def get_pos(data, regex, cur, ignore_cur=False, count=0): + """Return the position of `regex` match in `data`, starting at `cur`. + + Args: + data (str): the data to search in. + regex (pattern): regex pattern to search for. + cur (int): where to start the search. + ignore_cur (bool, optional): should the first match be ignored if it's + also the character at `cur`? + Defaults to False. + count (int, optional): the index of the match to return. Defaults to 0. + + Returns: + int: position of the match. -1 if no matches are found. + """ + # List of the *positions* of the found patterns. + matches = [m.start() for m in re.finditer(regex, data[cur:])] + pos = -1 + if count: + if len(matches) > count - 1: + if ignore_cur and matches[0] == 0: + if len(matches) > count: + pos = matches[count] + else: + pos = matches[count - 1] + elif matches: + if ignore_cur and matches[0] == 0: + if len(matches) > 1: + pos = matches[1] + else: + pos = matches[0] + return pos + +def set_cur(buf, input_line, pos, cap=True): + """Set the cursor's position. + + Args: + buf (str): pointer to the current WeeChat buffer. + input_line (str): the content of the input line. + pos (int): the position to set the cursor to. + cap (bool, optional): if True, the `pos` will shortened to the length + of `input_line` if it's too long. Defaults to True. + """ + if cap: + pos = min(pos, len(input_line) - 1) + weechat.buffer_set(buf, "input_pos", str(pos)) + +def start_catching_keys(amount, callback, input_line, cur, count, buf=None): + """Start catching keys. Used for special commands (e.g. "f", "r"). + + amount (int): amount of keys to catch. + callback (str): name of method to call once all keys are caught. + input_line (str): input line's content. + cur (int): cursor's position. + count (int): count, e.g. "2" for "2fs". + buf (str, optional): pointer to the current WeeChat buffer. + Defaults to None. + + `catching_keys_data` is a dict with the above arguments, as well as: + keys (str): pressed keys will be added under this key. + new_cur (int): the new cursor's position, set in the callback. + + When catching keys is active, normal pressed keys (e.g. "a" but not arrows) + will get added to `catching_keys_data` under the key "keys", and will not + be handled any further. + Once all keys are caught, the method defined in the "callback" key is + called, and can use the data in `catching_keys_data` to perform its action. + """ + global catching_keys_data + if "new_cur" in catching_keys_data: + new_cur = catching_keys_data['new_cur'] + catching_keys_data = {'amount': 0} + return new_cur, True, False + catching_keys_data = ({'amount': amount, + 'callback': callback, + 'input_line': input_line, + 'cur': cur, + 'keys': "", + 'count': count, + 'new_cur': 0, + 'buf': buf}) + return cur, False, True + +def get_keys_and_count(combo): + """Check if `combo` is a valid combo and extract keys/counts if so. + + Args: + combo (str): pressed keys combo. + + Returns: + matched (bool): True if the combo has a (partial or full) match, False + otherwise. + combo (str): `combo` with the count removed. These are the actual keys + we should handle. User mappings are also expanded. + count (int): count for `combo`. + """ + # Look for a potential match (e.g. "d" might become "dw" or "dd" so we + # accept it, but "d9" is invalid). + matched = False + # Digits are allowed at the beginning (counts or "0"). + count = 0 + if combo.isdigit(): + matched = True + elif combo and combo[0].isdigit(): + count = "" + for char in combo: + if char.isdigit(): + count += char + else: + break + combo = combo.replace(count, "", 1) + count = int(count) + # It's a user defined key. Expand it. + if combo in vimode_settings['user_mappings']: + combo = vimode_settings['user_mappings'][combo] + # It's a WeeChat command. + if not matched and combo.startswith("/"): + matched = True + # Check against defined keys. + if not matched: + for key in VI_KEYS: + if key.startswith(combo): + matched = True + break + # Check against defined motions. + if not matched: + for motion in VI_MOTIONS: + if motion.startswith(combo): + matched = True + break + # Check against defined operators + motions. + if not matched: + for operator in VI_OPERATORS: + if combo.startswith(operator): + # Check for counts before the motion (but after the operator). + vi_keys_no_op = combo[len(operator):] + # There's no motion yet. + if vi_keys_no_op.isdigit(): + matched = True + break + # Get the motion count, then multiply the operator count by + # it, similar to vim's behavior. + elif vi_keys_no_op and vi_keys_no_op[0].isdigit(): + motion_count = "" + for char in vi_keys_no_op: + if char.isdigit(): + motion_count += char + else: + break + # Remove counts from `vi_keys_no_op`. + combo = combo.replace(motion_count, "", 1) + motion_count = int(motion_count) + count = max(count, 1) * motion_count + # Check against defined motions. + for motion in VI_MOTIONS: + if motion.startswith(combo[1:]): + matched = True + break + return matched, combo, count + + +# Other helpers. +# -------------- + +def set_mode(arg): + """Set the current mode and update the bar mode indicator.""" + global mode + buf = weechat.current_buffer() + input_line = weechat.buffer_get_string(buf, "input") + if mode == "INSERT" and arg == "NORMAL": + add_undo_history(buf, input_line) + mode = arg + # If we're going to Normal mode, the cursor must move one character to the + # left. + if mode == "NORMAL": + cur = weechat.buffer_get_integer(buf, "input_pos") + set_cur(buf, input_line, cur - 1, False) + weechat.bar_item_update("mode_indicator") + +def cb_check_cmd_mode(data, remaining_calls): + """Exit command mode if user erases the leading ':' character.""" + buf = weechat.current_buffer() + cmd_text = weechat.buffer_get_string(buf, "input") + if not cmd_text: + set_mode("NORMAL") + return weechat.WEECHAT_RC_OK + +def add_undo_history(buf, input_line): + """Add an item to the per-buffer undo history.""" + if buf in undo_history: + if not undo_history[buf] or undo_history[buf][-1] != input_line: + undo_history[buf].append(input_line) + undo_history_index[buf] = -1 + else: + undo_history[buf] = ['', input_line] + undo_history_index[buf] = -1 + +def clear_undo_history(buf): + """Clear the undo history for a given buffer.""" + undo_history[buf] = [''] + undo_history_index[buf] = -1 + +def print_warning(text): + """Print warning, in red, to the current buffer.""" + weechat.prnt("", ("%s[vimode.py] %s" % (weechat.color("red"), text))) + +def check_warnings(): + """Warn the user about problematic key bindings and tmux/screen.""" + user_warned = False + # Warn the user about problematic key bindings that may conflict with + # vimode. + # The solution is to remove these key bindings, but that's up to the user. + infolist = weechat.infolist_get("key", "", "default") + problematic_keybindings = [] + while weechat.infolist_next(infolist): + key = weechat.infolist_string(infolist, "key") + command = weechat.infolist_string(infolist, "command") + if re.match(REGEX_PROBLEMATIC_KEYBINDINGS, key): + problematic_keybindings.append("%s -> %s" % (key, command)) + weechat.infolist_free(infolist) + if problematic_keybindings: + user_warned = True + print_warning("Problematic keybindings detected:") + for keybinding in problematic_keybindings: + print_warning(" %s" % keybinding) + print_warning("These keybindings may conflict with vimode.") + print_warning("You can remove problematic key bindings and add" + " recommended ones by using /vimode bind_keys, or only" + " list them with /vimode bind_keys --list") + print_warning("For help, see: %s" % FAQ_KEYBINDINGS) + del problematic_keybindings + # Warn tmux/screen users about possible Esc detection delays. + if "STY" in os.environ or "TMUX" in os.environ: + if user_warned: + weechat.prnt("", "") + user_warned = True + print_warning("tmux/screen users, see: %s" % FAQ_ESC) + if (user_warned and not + weechat.config_string_to_boolean(vimode_settings['no_warn'])): + if user_warned: + weechat.prnt("", "") + print_warning("To force disable warnings, you can set" + " plugins.var.python.vimode.no_warn to 'on'") + + +# Main script. +# ============ + +if __name__ == "__main__": + weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, + SCRIPT_LICENSE, SCRIPT_DESC, "", "") + # Warn the user if he's using an unsupported WeeChat version. + VERSION = weechat.info_get("version_number", "") + if int(VERSION) < 0x01000000: + print_warning("Please upgrade to WeeChat ≥ 1.0.0. Previous versions" + " are not supported.") + # Set up script options. + for option, value in list(vimode_settings.items()): + if weechat.config_is_set_plugin(option): + vimode_settings[option] = weechat.config_get_plugin(option) + else: + weechat.config_set_plugin(option, value[0]) + vimode_settings[option] = value[0] + weechat.config_set_desc_plugin(option, + "%s (default: \"%s\")" % (value[1], + value[0])) + load_user_mappings() + load_mode_colors() + # Warn the user about possible problems if necessary. + if not weechat.config_string_to_boolean(vimode_settings['no_warn']): + check_warnings() + # Create bar items and setup hooks. + weechat.bar_item_new("mode_indicator", "cb_mode_indicator", "") + weechat.bar_item_new("cmd_completion", "cb_cmd_completion", "") + weechat.bar_item_new("vi_buffer", "cb_vi_buffer", "") + weechat.bar_item_new("line_numbers", "cb_line_numbers", "") + if int(VERSION) >= 0x02090000: + weechat.bar_new("vi_line_numbers", "on", "0", "window", "", "left", + "vertical", "vertical", "0", "0", "default", "default", + "default", "default", "0", "line_numbers") + else: + weechat.bar_new("vi_line_numbers", "on", "0", "window", "", "left", + "vertical", "vertical", "0", "0", "default", "default", + "default", "0", "line_numbers") + weechat.hook_config("plugins.var.python.%s.*" % SCRIPT_NAME, "cb_config", + "") + weechat.hook_signal("key_pressed", "cb_key_pressed", "") + weechat.hook_signal("key_combo_default", "cb_key_combo_default", "") + weechat.hook_signal("key_combo_search", "cb_key_combo_search", "") + weechat.hook_signal("buffer_switch", "cb_update_line_numbers", "") + weechat.hook_command("vimode", SCRIPT_DESC, "[help | bind_keys [--list]]", + " help: show help\n" + "bind_keys: unbind problematic keys, and bind" + " recommended keys to use in WeeChat\n" + " --list: only list changes", + "help || bind_keys |--list", + "cb_vimode_cmd", "") + weechat.hook_command("vimode_go_to_normal", + ("This command can be used for key bindings to go to " + "normal mode."), + "", "", "", "cb_vimode_go_to_normal", "") + # Remove obsolete bar. + vi_cmd_bar = weechat.bar_search("vi_cmd") + weechat.bar_remove(vi_cmd_bar) diff --git a/.config/wgetrc b/.config/wgetrc old mode 100644 new mode 100755 diff --git a/.config/zsh/.zshrc b/.config/zsh/.zshrc index 16f8db77..3a8a0427 100644 --- a/.config/zsh/.zshrc +++ b/.config/zsh/.zshrc @@ -1,6 +1,6 @@ #zmodload zsh/zprof eval "$(direnv hook zsh)" >> $XDG_RUNTIME_DIR/direnv -paleofetch +#paleofetch if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" fi diff --git a/.config/zsh/aliases b/.config/zsh/aliases index 80c21cfc..caf12a95 100755 --- a/.config/zsh/aliases +++ b/.config/zsh/aliases @@ -1,9 +1,6 @@ #!/usr/bin/zsh alias feh="feh --scale-down --auto-zoom --no-fehbg" -alias cclear="$(which clear)" -alias clear="clear && paleofetch" -alias neofetch="neofetch --ascii ~/.config/neofetch/ascii.txt" alias open=xdg-open alias rm="rm -i" alias clip="xclip -selection clipboard" @@ -28,7 +25,7 @@ weechat(){ } # Suffix aliases -alias -g G=" | rg" +alias -g G=" | rg -i " alias gshh="gcloud cloud-shell ssh --authorize-session" # Git @@ -52,3 +49,5 @@ alias dpall="dots remote | xargs -I R git --git-dir=$HOME/.dotfiles.git/ --work- alias dignore="dots update-index --assume-unchanged {pkg.list,install.sh,README.md}" alias da="dots add -u" alias dcm="dots commit" + +alias bon="sudo bash -c 'rc-service bluetooth start && rfkill unblock bluetooth'" diff --git a/.local/bin/backup b/.local/bin/backup index 9662f126..c75b9017 100755 --- a/.local/bin/backup +++ b/.local/bin/backup @@ -20,7 +20,8 @@ date=$(date "+%I:%M-%d.%m.%Y") DIRS="/home/yigit /etc /var/lib -/var/spool" +/var/spool +/usr/src/linux" # Check whether backup server is available on network timeout 3 ssh -i "$SSH_KEY" $SSH_USER@$SSH_HOST id < /dev/null > /dev/null 2> /dev/null || echo "SSH Failed." || exit diff --git a/.local/bin/mpd-notif b/.local/bin/mpd-notif index 34a7d4c7..f5987433 100755 --- a/.local/bin/mpd-notif +++ b/.local/bin/mpd-notif @@ -1,6 +1,6 @@ #!/bin/sh -title=$(mpc -f "%title%" 2> /dev/null | head -n 1 | xargs) -artist=$(mpc -f "%artist%" 2> /dev/null | head -n 1 | xargs) +title=$(mpc -f "%title%" 2> /dev/null | head -n 1 | xargs -0 ) +artist=$(mpc -f "%artist%" 2> /dev/null | head -n 1 | xargs -0 ) notify-send -a " $artist" -t 1500 "~ $title ~ " diff --git a/.local/bin/status-bar/cpu-temp b/.local/bin/status-bar/cpu-temp index c3c5405f..dee2f9d8 100755 --- a/.local/bin/status-bar/cpu-temp +++ b/.local/bin/status-bar/cpu-temp @@ -1,8 +1,14 @@ #!/bin/bash -source ~/.config/config.env +i=0 +max=0 +while [ -f "/sys/class/thermal/thermal_zone$i/temp" ]; do + if [ "$(sed 's/[0-9][0-9][0-9]$//' "/sys/class/thermal/thermal_zone$i/temp")" -gt "$max" ]; then + max="$(sed 's/[0-9][0-9][0-9]$//' "/sys/class/thermal/thermal_zone$i/temp")" + fi + i=$((i+1)) +done -temp=$(sed 's/[0-9][0-9][0-9]$/°C/' $TEMP_ZONE) icon= -echo "^c#bf616a^$icon^d^ $temp" +echo "^c#bf616a^$icon^d^ $max°C" diff --git a/.local/bin/status-bar/volume b/.local/bin/status-bar/volume index 8fab85dc..cfc5dd83 100755 --- a/.local/bin/status-bar/volume +++ b/.local/bin/status-bar/volume @@ -14,5 +14,5 @@ else fi case $BLOCK_BUTTON in - 1) setsid -f st -c stpulse -n stpulse -e ncpamixer ;; + 1) setsid -f st -c stpulse -n stpulse -e pamix ;; esac diff --git a/.local/share/dwm/autostart.sh b/.local/share/dwm/autostart.sh new file mode 100755 index 00000000..64aa09d1 --- /dev/null +++ b/.local/share/dwm/autostart.sh @@ -0,0 +1,54 @@ +#!/bin/sh + + +~/.local/bin/daily-update +dwmblocks > $XDG_RUNTIME_DIR/dwmblocks.out 2> $XDG_RUNTIME_DIR/dwmblocks.err & + +~/.local/bin/keyboard > $XDG_RUNTIME_DIR/keyboard.out 2> $XDG_RUNTIME_DIR/keyboard.err +~/.local/bin/mailsync & + +if [ "$NEXTCLOUD" = true ] ; then + nextcloud --background & +fi +mkdir -p ~/Downloads/neomutt +if [ "$MCONNECT" = true ] ; then + mkdir -p ~/Downloads/mconnect + (cd ~/Downloads/mconnect; mconnect -d > $XDG_RUNTIME_DIR/mconnect 2> $XDG_RUNTIME_DIR/mconnect.err &) +fi +if [ "$ACTIVITYWATCHER" = true ] ; then + pkill -f aw-watcher-window + pkill -f aw-watcher-afk + pkill -f aw-server + aw-server & + aw-watcher-window & + aw-watcher-afk & +fi +mpd +mpd-mpris & +touch ~/.cache/nextcloud-track +xss-lock -- slock & +picom --no-fading-openclose & + + +~/.local/bin/firefox-sync +curl 'http://yeetclock/setcolor?R=136&G=192&B=208' & + +dunst & + +xbanish -s & + +redshift -x 2> /dev/null > /dev/null +redshift -r -l "$LATLONG" > /dev/null 2> /dev/null & + + +tmux new-session -s weechat -d weechat > /dev/null 2> /dev/null + + +~/.local/bin/devmon --exec-on-drive "/sbin/notify-send '禍 drive mounted' '%l (%f) at %d '" \ + --exec-on-remove "/sbin/notify-send '禍 drive removed' '%l (%f) from %d '" \ + --exec-on-unmount "/sbin/notify-send '禍 drive unmounted' '%l (%f) from %d '" \ + --no-unmount --no-gui & + +clipmenud > $XDG_RUNTIME_DIR/clipmenud.out 2> $XDG_RUNTIME_DIR/clipmenud.err & +rm -f ~/.surf/tabbed-surf.xid +/bin/polkit-dumb-agent & diff --git a/.local/share/gnupg/gpg-agent.conf b/.local/share/gnupg/gpg-agent.conf index 80e2992c..f87cd6a1 100644 --- a/.local/share/gnupg/gpg-agent.conf +++ b/.local/share/gnupg/gpg-agent.conf @@ -1,3 +1,5 @@ +pinentry-program /usr/bin/pinentry +no-grab allow-preset-passphrase max-cache-ttl 172800 enable-ssh-support diff --git a/.local/src/dwm/dwm.1 b/.local/src/dwm/dwm.1 index 829047b8..53cabd88 100644 --- a/.local/src/dwm/dwm.1 +++ b/.local/src/dwm/dwm.1 @@ -30,6 +30,14 @@ top left corner. The tags which are applied to one or more windows are indicated with an empty square in the top left corner. .P dwm draws a small border around windows to indicate the focus state. +.P +On start, dwm can start additional programs that may be specified in two special +shell scripts (see the FILES section below), autostart_blocking.sh and +autostart.sh. The former is executed first and dwm will wait for its +termination before starting. The latter is executed in the background before +dwm enters its handler loop. +.P +Either of these files may be omitted. .SH OPTIONS .TP .B \-v @@ -158,6 +166,21 @@ Toggles focused window between floating and tiled state. .TP .B Mod1\-Button3 Resize focused window while dragging. Tiled windows will be toggled to the floating state. +.SH FILES +The files containing programs to be started along with dwm are searched for in +the following directories: +.IP "1. $XDG_DATA_HOME/dwm" +.IP "2. $HOME/.local/share/dwm" +.IP "3. $HOME/.dwm" +.P +The first existing directory is scanned for any of the autostart files below. +.TP 15 +autostart.sh +This file is started as a shell background process before dwm enters its handler +loop. +.TP 15 +autostart_blocking.sh +This file is started before any autostart.sh; dwm waits for its termination. .SH CUSTOMIZATION dwm is customized by creating a custom config.h and (re)compiling the source code. This keeps it fast, secure and simple. diff --git a/.local/src/dwm/dwm.c b/.local/src/dwm/dwm.c index 25bf3cd1..c3d12680 100644 --- a/.local/src/dwm/dwm.c +++ b/.local/src/dwm/dwm.c @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -231,6 +232,7 @@ static void resizeclient(Client *c, int x, int y, int w, int h); static void resizemouse(const Arg *arg); static void restack(Monitor *m); static void run(void); +static void runautostart(void); static void scan(void); static int sendevent(Client *c, Atom proto); static void sendmon(Client *c, Monitor *m); @@ -285,7 +287,11 @@ static Client *termforwin(const Client *c); static pid_t winpid(Window w); /* variables */ +static const char autostartblocksh[] = "autostart_blocking.sh"; +static const char autostartsh[] = "autostart.sh"; static const char broken[] = "broken"; +static const char dwmdir[] = "dwm"; +static const char localshare[] = ".local/share"; static char stext[1024]; static char rawstext[1024]; static int dwmblockssig; @@ -2016,6 +2022,83 @@ run(void) handler[ev.type](&ev); /* call handler */ } +void +runautostart(void) +{ + char *pathpfx; + char *path; + char *xdgdatahome; + char *home; + struct stat sb; + + if ((home = getenv("HOME")) == NULL) + /* this is almost impossible */ + return; + + /* if $XDG_DATA_HOME is set and not empty, use $XDG_DATA_HOME/dwm, + * otherwise use ~/.local/share/dwm as autostart script directory + */ + xdgdatahome = getenv("XDG_DATA_HOME"); + if (xdgdatahome != NULL && *xdgdatahome != '\0') { + /* space for path segments, separators and nul */ + pathpfx = ecalloc(1, strlen(xdgdatahome) + strlen(dwmdir) + 2); + + if (sprintf(pathpfx, "%s/%s", xdgdatahome, dwmdir) <= 0) { + free(pathpfx); + return; + } + } else { + /* space for path segments, separators and nul */ + pathpfx = ecalloc(1, strlen(home) + strlen(localshare) + + strlen(dwmdir) + 3); + + if (sprintf(pathpfx, "%s/%s/%s", home, localshare, dwmdir) < 0) { + free(pathpfx); + return; + } + } + + /* check if the autostart script directory exists */ + if (! (stat(pathpfx, &sb) == 0 && S_ISDIR(sb.st_mode))) { + /* the XDG conformant path does not exist or is no directory + * so we try ~/.dwm instead + */ + char *pathpfx_new = realloc(pathpfx, strlen(home) + strlen(dwmdir) + 3); + if(pathpfx_new == NULL) { + free(pathpfx); + return; + } + pathpfx = pathpfx_new; + + if (sprintf(pathpfx, "%s/.%s", home, dwmdir) <= 0) { + free(pathpfx); + return; + } + } + + /* try the blocking script first */ + path = ecalloc(1, strlen(pathpfx) + strlen(autostartblocksh) + 2); + if (sprintf(path, "%s/%s", pathpfx, autostartblocksh) <= 0) { + free(path); + free(pathpfx); + } + + if (access(path, X_OK) == 0) + system(path); + + /* now the non-blocking script */ + if (sprintf(path, "%s/%s", pathpfx, autostartsh) <= 0) { + free(path); + free(pathpfx); + } + + if (access(path, X_OK) == 0) + system(strcat(path, " &")); + + free(pathpfx); + free(path); +} + void scan(void) { @@ -3074,6 +3157,7 @@ main(int argc, char *argv[]) die("pledge"); #endif /* __OpenBSD__ */ scan(); + runautostart(); run(); cleanup(); XCloseDisplay(dpy); diff --git a/.local/src/dwm/patches/dwm-autostart-20210120-cb3f58a.diff b/.local/src/dwm/patches/dwm-autostart-20210120-cb3f58a.diff new file mode 100644 index 00000000..efee6761 --- /dev/null +++ b/.local/src/dwm/patches/dwm-autostart-20210120-cb3f58a.diff @@ -0,0 +1,179 @@ +From 37e970479dc5d40e57fc0cbfeaa5e39941483237 Mon Sep 17 00:00:00 2001 +From: Gan Ainm +Date: Wed, 10 Jun 2020 10:59:02 +0000 +Subject: [PATCH] dwm-xdgautostart-6.2.diff + +=================================================================== +--- + dwm.1 | 23 +++++++++++++++++ + dwm.c | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + 2 files changed, 105 insertions(+) + +diff --git a/dwm.1 b/dwm.1 +index 13b3729..9533aa6 100644 +--- a/dwm.1 ++++ b/dwm.1 +@@ -30,6 +30,14 @@ top left corner. The tags which are applied to one or more windows are + indicated with an empty square in the top left corner. + .P + dwm draws a small border around windows to indicate the focus state. ++.P ++On start, dwm can start additional programs that may be specified in two special ++shell scripts (see the FILES section below), autostart_blocking.sh and ++autostart.sh. The former is executed first and dwm will wait for its ++termination before starting. The latter is executed in the background before ++dwm enters its handler loop. ++.P ++Either of these files may be omitted. + .SH OPTIONS + .TP + .B \-v +@@ -152,6 +160,21 @@ Toggles focused window between floating and tiled state. + .TP + .B Mod1\-Button3 + Resize focused window while dragging. Tiled windows will be toggled to the floating state. ++.SH FILES ++The files containing programs to be started along with dwm are searched for in ++the following directories: ++.IP "1. $XDG_DATA_HOME/dwm" ++.IP "2. $HOME/.local/share/dwm" ++.IP "3. $HOME/.dwm" ++.P ++The first existing directory is scanned for any of the autostart files below. ++.TP 15 ++autostart.sh ++This file is started as a shell background process before dwm enters its handler ++loop. ++.TP 15 ++autostart_blocking.sh ++This file is started before any autostart.sh; dwm waits for its termination. + .SH CUSTOMIZATION + dwm is customized by creating a custom config.h and (re)compiling the source + code. This keeps it fast, secure and simple. +diff --git a/dwm.c b/dwm.c +index 4465af1..2156b49 100644 +--- a/dwm.c ++++ b/dwm.c +@@ -29,6 +29,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -193,6 +194,7 @@ static void resizeclient(Client *c, int x, int y, int w, int h); + static void resizemouse(const Arg *arg); + static void restack(Monitor *m); + static void run(void); ++static void runautostart(void); + static void scan(void); + static int sendevent(Client *c, Atom proto); + static void sendmon(Client *c, Monitor *m); +@@ -235,7 +237,11 @@ static int xerrorstart(Display *dpy, XErrorEvent *ee); + static void zoom(const Arg *arg); + + /* variables */ ++static const char autostartblocksh[] = "autostart_blocking.sh"; ++static const char autostartsh[] = "autostart.sh"; + static const char broken[] = "broken"; ++static const char dwmdir[] = "dwm"; ++static const char localshare[] = ".local/share"; + static char stext[256]; + static int screen; + static int sw, sh; /* X display screen geometry width, height */ +@@ -1380,6 +1386,83 @@ run(void) + handler[ev.type](&ev); /* call handler */ + } + ++void ++runautostart(void) ++{ ++ char *pathpfx; ++ char *path; ++ char *xdgdatahome; ++ char *home; ++ struct stat sb; ++ ++ if ((home = getenv("HOME")) == NULL) ++ /* this is almost impossible */ ++ return; ++ ++ /* if $XDG_DATA_HOME is set and not empty, use $XDG_DATA_HOME/dwm, ++ * otherwise use ~/.local/share/dwm as autostart script directory ++ */ ++ xdgdatahome = getenv("XDG_DATA_HOME"); ++ if (xdgdatahome != NULL && *xdgdatahome != '\0') { ++ /* space for path segments, separators and nul */ ++ pathpfx = ecalloc(1, strlen(xdgdatahome) + strlen(dwmdir) + 2); ++ ++ if (sprintf(pathpfx, "%s/%s", xdgdatahome, dwmdir) <= 0) { ++ free(pathpfx); ++ return; ++ } ++ } else { ++ /* space for path segments, separators and nul */ ++ pathpfx = ecalloc(1, strlen(home) + strlen(localshare) ++ + strlen(dwmdir) + 3); ++ ++ if (sprintf(pathpfx, "%s/%s/%s", home, localshare, dwmdir) < 0) { ++ free(pathpfx); ++ return; ++ } ++ } ++ ++ /* check if the autostart script directory exists */ ++ if (! (stat(pathpfx, &sb) == 0 && S_ISDIR(sb.st_mode))) { ++ /* the XDG conformant path does not exist or is no directory ++ * so we try ~/.dwm instead ++ */ ++ char *pathpfx_new = realloc(pathpfx, strlen(home) + strlen(dwmdir) + 3); ++ if(pathpfx_new == NULL) { ++ free(pathpfx); ++ return; ++ } ++ pathpfx = pathpfx_new; ++ ++ if (sprintf(pathpfx, "%s/.%s", home, dwmdir) <= 0) { ++ free(pathpfx); ++ return; ++ } ++ } ++ ++ /* try the blocking script first */ ++ path = ecalloc(1, strlen(pathpfx) + strlen(autostartblocksh) + 2); ++ if (sprintf(path, "%s/%s", pathpfx, autostartblocksh) <= 0) { ++ free(path); ++ free(pathpfx); ++ } ++ ++ if (access(path, X_OK) == 0) ++ system(path); ++ ++ /* now the non-blocking script */ ++ if (sprintf(path, "%s/%s", pathpfx, autostartsh) <= 0) { ++ free(path); ++ free(pathpfx); ++ } ++ ++ if (access(path, X_OK) == 0) ++ system(strcat(path, " &")); ++ ++ free(pathpfx); ++ free(path); ++} ++ + void + scan(void) + { +@@ -2142,6 +2223,7 @@ main(int argc, char *argv[]) + die("pledge"); + #endif /* __OpenBSD__ */ + scan(); ++ runautostart(); + run(); + cleanup(); + XCloseDisplay(dpy); +-- +2.27.0 + diff --git a/.local/src/dwm/patches/dwm-losefullscreen-6.2.diff b/.local/src/dwm/patches/dwm-losefullscreen-6.2.diff new file mode 100644 index 00000000..990bf732 --- /dev/null +++ b/.local/src/dwm/patches/dwm-losefullscreen-6.2.diff @@ -0,0 +1,98 @@ +From b0f56cb0228afc5bda80ea233d1cffe1a19f292f Mon Sep 17 00:00:00 2001 +From: bakkeby +Date: Tue, 7 Apr 2020 11:40:30 +0200 +Subject: [PATCH] Lose fullscreen on focus change + +--- + dwm.c | 20 ++++++++++++++------ + 1 file changed, 14 insertions(+), 6 deletions(-) + +diff --git a/dwm.c b/dwm.c +index 4465af1..a300ba0 100644 +--- a/dwm.c ++++ b/dwm.c +@@ -418,15 +418,16 @@ buttonpress(XEvent *e) + { + unsigned int i, x, click; + Arg arg = {0}; +- Client *c; ++ Client *c, *sel; + Monitor *m; + XButtonPressedEvent *ev = &e->xbutton; + + click = ClkRootWin; + /* focus monitor if necessary */ + if ((m = wintomon(ev->window)) && m != selmon) { +- unfocus(selmon->sel, 1); ++ sel = selmon->sel; + selmon = m; ++ unfocus(sel, 1); + focus(NULL); + } + if (ev->window == selmon->barwin) { +@@ -754,7 +755,7 @@ drawbars(void) + void + enternotify(XEvent *e) + { +- Client *c; ++ Client *c, *sel; + Monitor *m; + XCrossingEvent *ev = &e->xcrossing; + +@@ -763,8 +764,9 @@ enternotify(XEvent *e) + c = wintoclient(ev->window); + m = c ? c->mon : wintomon(ev->window); + if (m != selmon) { +- unfocus(selmon->sel, 1); ++ sel = selmon->sel; + selmon = m; ++ unfocus(sel, 1); + } else if (!c || c == selmon->sel) + return; + focus(c); +@@ -819,13 +821,15 @@ void + focusmon(const Arg *arg) + { + Monitor *m; ++ Client *sel; + + if (!mons->next) + return; + if ((m = dirtomon(arg->i)) == selmon) + return; +- unfocus(selmon->sel, 0); ++ sel = selmon->sel; + selmon = m; ++ unfocus(sel, 0); + focus(NULL); + } + +@@ -1120,13 +1124,15 @@ motionnotify(XEvent *e) + { + static Monitor *mon = NULL; + Monitor *m; ++ Client *sel; + XMotionEvent *ev = &e->xmotion; + + if (ev->window != root) + return; + if ((m = recttomon(ev->x_root, ev->y_root, 1, 1)) != mon && mon) { +- unfocus(selmon->sel, 1); ++ sel = selmon->sel; + selmon = m; ++ unfocus(sel, 1); + focus(NULL); + } + mon = m; +@@ -1751,6 +1757,8 @@ unfocus(Client *c, int setfocus) + { + if (!c) + return; ++ if (c->isfullscreen && ISVISIBLE(c) && c->mon == selmon) ++ setfullscreen(c, 0); + grabbuttons(c, 0); + XSetWindowBorder(dpy, c->win, scheme[SchemeNorm][ColBorder].pixel); + if (setfocus) { +-- +2.19.1 + diff --git a/.local/src/dwm/rules.h b/.local/src/dwm/rules.h index e7a11d8a..6a5377c6 100644 --- a/.local/src/dwm/rules.h +++ b/.local/src/dwm/rules.h @@ -20,7 +20,7 @@ static const Rule rules[] = { /* class instance title tags mask isfloating floatpos isterminal noswallow monitor */ { "discord" , NULL , NULL , 1 << 8 , NULL , NULL , 0 , 0 , -1 }, { "Brave-browser" , NULL , NULL , 1 << 1 , NULL , NULL , 0 , 0 , -1 }, -{ "firefox" , NULL , NULL , 1 << 1 , NULL , NULL , 0 , 0 , -1 }, +{ "Firefox" , NULL , NULL , 1 << 1 , NULL , NULL , 0 , 0 , -1 }, { "tabbed-surf" , NULL , NULL , 1 << 1 , NULL , NULL , 0 , 0 , -1 }, { "bitwarden" , NULL , NULL , 1 << 6 , NULL , NULL , 0 , 0 , -1 }, { "QtPass" , NULL , NULL , 1 << 6 , NULL , NULL , 0 , 0 , -1 }, diff --git a/.local/src/dwmblocks/config.h b/.local/src/dwmblocks/config.h index eba1199d..c28be264 100644 --- a/.local/src/dwmblocks/config.h +++ b/.local/src/dwmblocks/config.h @@ -16,7 +16,7 @@ static Block blocks[] = { { "", PATH("cpu-temp"), 30, 17}, { "", PATH("memory"), 120, 21}, { "", PATH("weather"), 60, 16}, - { "", PATH("arch"), 120, 15}, +// { "", PATH("arch"), 120, 15}, { "", PATH("volume"), 5, 14}, { "", PATH("network"), 120, 13}, { "", PATH("battery"), 60, 12}, diff --git a/.local/src/grabc/grabc b/.local/src/grabc/grabc index 4962cc06747fd18885e781421ed66c42cb4e20da..f582183334f0c6545e079b648c51b435fdeab5cc 100755 GIT binary patch delta 5970 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 diff --git a/.local/src/grabc/grabc.o b/.local/src/grabc/grabc.o index 8a99c567dc0bf63d7241dbbcfbec7ff65e18f63c..3c572a80a411dc4d90231af5daed295dbcf2b1d9 100644 GIT binary patch delta 4719 zcmaJ^4RBP|6@GV~pi^7bhq8T!6h zEWR~+MD2BK6wW zix=sXy(qnN?IRg;H6Pe39i^e6bat3SeRdZ$C1;DHv^jZ|7^0En>fDKIY=5>j^4=Fl zJxuQ=FBF60pCqpl&X$j((bxh%wWT!JX8%JYDJ#WsdM%~Uow@HJIb7!PR&Q=I=X{x) zy?Ho3FI!s4BgO&^oXOlb@3Uz1{|cOTX{pVer){aF&gp+gqb-q{mdI3O=6_rZ=3Kw= zX`H&m$iiHUcf^8D1nVN?W(aPnpR`f z)El{lJoy!3v+?)spQqwNWA@7f#I+S{>oS;75uu)r$<^qT^`-X*6g_rQoMz7b?3 z$|EQb3vW@l>`n^+zK$-qb$?qN_D*nH1F9V)Ji2(Adfd9}Flc#H-U*IlH0stn&cv|R ze9nP$v#hr8Hvw+}JPP=i0AEC&Lfv1v@5LABww;cMQbo4z5HLXx{ zMB!|Wb2|xmKGdHApCI7BhkEpxH1Wk653mRXQ)onsmgq~drN0B)1ghoKxI}kVbEh)c zlRxJ27a8BA9?3og>|%*pTQxv_ufCQP%cm~``wXZG03QY14fspsS*mv&2i61Z%L?aH z8{n@%eIelI;?%!EvtGULtvKu?xOu^CDo*`nsJ{v7KLmU!;87FbYAp0dAX1FSgt_>) z0iLCB-WaTRLj5MFUrmigdS4LO4Zv;yH=YS!haBI7`rGAqYl9z-sn0R0HSJNr@!EXe z#QTi)ioxhG3=jnT1mJf7{yq#)0{G{Emja%uo)hLWt)A;3M-kjDuIo||U90vFs7~zDjgAv~Iwqz-m!TaNuhqW{rfa64S6>e@D zeOQtz*3p<;iA^A2GFu)TJ3xRJQW)?pvd$WK0PtqO9|ODv@G%oNS7HSauPXs}Rsr~B zz_GKdfcv2SGlg?gLBKQS$uBb4KfsT7tAhgSSf=~EijI|)eL*&-x`k$?sxgjgV;t2z z<{+`0kR_E$JHt{va8t@yBW%MZ+1p&`WkzZu6 z5x>z`^vM}%S(pS+77BZQ2hk(m4KfHpF06ROJj1e{tBv_ft>{$f6%Xk8{V2W z<1e<^3)UwqoV&mWK)Q)jUAbdzIcQhG9Bu|2ugxa##5vqgJPweP2;7#;g`9jw*coNCxjBAIlpCDZ**ImuL z%rer<(+xI6VukK%Rjl=RWE~%0A-`ldDb{+}TI?3;knCNG^~N}FqhZNzRV@Fwb6DC= znvm=+#qvGDVX=p3R;c8{ zE^%1wb2KK|Clq^^N#0-&(k00rQtb8^_62hIsJABI@bTk8My~qDokHUW3|Pu$7C_jm)93#koUT3sG66bUSC$gu7YP|}(zUv}ly2qlv@xHw>a_m?!)bRQ delta 4813 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 a<=LDnM3tVl5AxU`&t;=U{vM`Jp#KBITAttl diff --git a/.local/src/st/st b/.local/src/st/st index e92a2f83fcc58e933a52fe523a45f634a5f0b6d6..8737adcde004baaef2ffa0a6dcd4160289e12653 100755 GIT binary patch delta 37620 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≪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 delta 37902 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 Wznlt_dw+Dl>D?-`5gR8#um1xC)>Kvi delta 5617 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 QlJxL{n)eX