summaryrefslogtreecommitdiff
path: root/.weechat/python/vimode.py
diff options
context:
space:
mode:
authorVito Graffagnino <vito@graffagnino.xyz>2020-09-08 18:10:49 +0100
committerVito Graffagnino <vito@graffagnino.xyz>2020-09-08 18:10:49 +0100
commit3b0142cedcde39e4c2097ecd916a870a3ced5ec6 (patch)
tree2116c49a845dfc0945778f2aa3e2118d72be428b /.weechat/python/vimode.py
parent8cc927e930d5b6aafe3e9862a61e81705479a1b4 (diff)
Added the relevent parts of the .config directory. Alss add ssh config
Diffstat (limited to '.weechat/python/vimode.py')
-rw-r--r--.weechat/python/vimode.py1877
1 files changed, 1877 insertions, 0 deletions
diff --git a/.weechat/python/vimode.py b/.weechat/python/vimode.py
new file mode 100644
index 0000000..b7dc35c
--- /dev/null
+++ b/.weechat/python/vimode.py
@@ -0,0 +1,1877 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2013-2014 Germain Z. <germanosz@gmail.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+#
+
+#
+# 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 <germanosz@gmail.com>"
+SCRIPT_VERSION = "0.7"
+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 <Esc>` 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"
+ " <Enter> 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("<Left>", re.IGNORECASE): '\x01[[D',
+ re.compile("<Right>", re.IGNORECASE): '\x01[[C',
+ re.compile("<Up>", re.IGNORECASE): '\x01[[A',
+ re.compile("<Down>", re.IGNORECASE): '\x01[[B'
+}
+REGEX_MAP_KEYS_2 = {
+ re.compile(r"<C-([^>]*)>", re.IGNORECASE): '\x01\\1',
+ re.compile(r"<M-([^>]*)>", 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: <Up>, <Down>, <Left>, <Right>, <C-...> and <M-...>.
+
+ 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<num> 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<num> 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<number> 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 <Return>.
+ 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 <command>``)."""
+ # ``/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)
+ 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))
+ 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", "")
+ 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)