forked from DustVoice/dotfiles
1883 lines
68 KiB
Python
1883 lines
68 KiB
Python
|
# -*- 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.8"
|
||
|
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", "")
|
||
|
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)
|