Skip to content

Commit b51825d

Browse files
GermainZflashcode
authored andcommitted
New script lossage.py: displays the last few input keystrokes and the commands run
1 parent 414cff3 commit b51825d

File tree

1 file changed

+158
-0
lines changed

1 file changed

+158
-0
lines changed

python/lossage.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright (C) 2021 Germain Z. <[email protected]>
4+
#
5+
# This program is free software; you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation; either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License
16+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
#
18+
19+
#
20+
# Inspired by Emacs's view-lossage, this script displays a history of the last
21+
# keystrokes you performed and the commands invoked.
22+
#
23+
24+
25+
import collections
26+
import dataclasses
27+
import re
28+
from typing import Deque, Dict, Iterable, Tuple
29+
30+
import weechat # type: ignore # pylint: disable=import-error
31+
32+
33+
SCRIPT_NAME = "lossage"
34+
SCRIPT_AUTHOR = "GermainZ <[email protected]>"
35+
SCRIPT_VERSION = "0.1"
36+
SCRIPT_LICENSE = "GPL3"
37+
SCRIPT_DESC = (
38+
"Displays the last few input keystrokes and the commands run, "
39+
"inspired by Emacs's view-lossage."
40+
)
41+
42+
43+
HISTORY_SIZE: int = 300
44+
REGEX_COMBO_REPL: Iterable[Tuple[re.Pattern, str]] = (
45+
(re.compile(r"\x01\[\["), "meta2-"),
46+
(re.compile(r"\x01\["), "meta-"),
47+
(re.compile(r"\x01"), "ctrl-"),
48+
)
49+
REGEX_AREA_STRIP = re.compile(r"^@[^:]+:")
50+
HEADER: Tuple[str, str, str] = ("context", "combo", "command")
51+
52+
53+
@dataclasses.dataclass
54+
class HistoryItem:
55+
context: str
56+
combo: str
57+
command: str
58+
59+
60+
@dataclasses.dataclass
61+
class Data:
62+
history: Deque[HistoryItem] = dataclasses.field(
63+
default_factory=lambda: collections.deque(maxlen=HISTORY_SIZE)
64+
)
65+
key_bindings: Dict[str, Dict[str, str]] = dataclasses.field(
66+
default_factory=lambda: collections.defaultdict(dict)
67+
)
68+
69+
70+
DATA = Data()
71+
72+
73+
def cb_key_combo(context: str, signal: str, signal_data: str):
74+
mode = signal.split("_")[-1]
75+
combo = signal_data
76+
77+
for regex, repl in REGEX_COMBO_REPL:
78+
combo = regex.sub(repl, combo)
79+
80+
command = DATA.key_bindings[context].get(combo, "")
81+
DATA.history.append(HistoryItem(mode, combo, command))
82+
return weechat.WEECHAT_RC_OK
83+
84+
85+
def cb_lossage_cmd(*_):
86+
buffer = weechat.buffer_search("python", SCRIPT_NAME)
87+
if not buffer:
88+
buffer = weechat.buffer_new(SCRIPT_NAME, "", "", "", "")
89+
weechat.buffer_set(buffer, "localvar_set_no_log", "1")
90+
weechat.buffer_set(buffer, "time_for_each_line", "0")
91+
weechat.buffer_set(buffer, "nicklist", "0")
92+
weechat.command(buffer, f"/buffer {SCRIPT_NAME}")
93+
weechat.command(buffer, "/buffer clear")
94+
95+
weechat.prnt(
96+
buffer,
97+
f"{weechat.color('bold')}{HEADER[0]:10} {HEADER[1]:20} {HEADER[2]}",
98+
)
99+
for item in DATA.history:
100+
if item is None:
101+
break
102+
weechat.prnt(
103+
buffer, f"{item.context:10} {item.combo:20} {item.command}"
104+
)
105+
106+
return weechat.WEECHAT_RC_OK
107+
108+
109+
def cb_key_bindings_changed(*_):
110+
populate_key_bindings()
111+
112+
113+
def populate_key_bindings():
114+
for context in ("default", "search", "cursor"):
115+
infolist = weechat.infolist_get("key", "", context)
116+
117+
while weechat.infolist_next(infolist):
118+
key = weechat.infolist_string(infolist, "key")
119+
command = weechat.infolist_string(infolist, "command")
120+
121+
# In the cursor context, bindings of the form `@area:key` can be
122+
# created. When the binding is used, the area is not passed to the
123+
# key_combo_* signal, so the best we can do is print a helpful
124+
# warning if there are several possible matches.
125+
if context == "cursor":
126+
key = REGEX_AREA_STRIP.sub("", key)
127+
if DATA.key_bindings[context].get(key, None):
128+
command = (
129+
f"ambiguous; see `/key list {context}` -> "
130+
f"`@…:{key}` key bindings"
131+
)
132+
133+
DATA.key_bindings[context][key] = command
134+
135+
weechat.infolist_free(infolist)
136+
137+
138+
if __name__ == "__main__":
139+
weechat.register(
140+
SCRIPT_NAME,
141+
SCRIPT_AUTHOR,
142+
SCRIPT_VERSION,
143+
SCRIPT_LICENSE,
144+
SCRIPT_DESC,
145+
"",
146+
"",
147+
)
148+
149+
populate_key_bindings()
150+
151+
weechat.hook_signal("key_combo_default", "cb_key_combo", "default")
152+
weechat.hook_signal("key_combo_search", "cb_key_combo", "search")
153+
weechat.hook_signal("key_combo_cursor", "cb_key_combo", "cursor")
154+
weechat.hook_command(
155+
"lossage", SCRIPT_DESC, "", "test", "", "cb_lossage_cmd", ""
156+
)
157+
weechat.hook_signal("key_bind", "cb_key_bindings_changed", "")
158+
weechat.hook_signal("key_unbind", "cb_key_bindings_changed", "")

0 commit comments

Comments
 (0)