]> git.rmz.io Git - dotfiles.git/blob - weechat/python/go.py
vim: don't duplicate plugins
[dotfiles.git] / weechat / python / go.py
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright (C) 2009-2023 Sébastien Helleu <flashcode@flashtux.org>
4 # Copyright (C) 2010 m4v <lambdae2@gmail.com>
5 # Copyright (C) 2011 stfn <stfnmd@googlemail.com>
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 #
20
21 #
22 # History:
23 #
24 # 2023-06-21, Sébastien Helleu <flashcode@flashtux.org>:
25 # version 2.9: add option "min_chars"
26 # 2023-01-08, Sébastien Helleu <flashcode@flashtux.org>:
27 # version 2.8: send buffer pointer with signal "input_text_changed"
28 # 2021-05-25, Tomáš Janoušek <tomi@nomi.cz>:
29 # version 2.7: add new option to prefix short names with server names
30 # 2019-07-11, Simmo Saan <simmo.saan@gmail.com>
31 # version 2.6: fix detection of "/input search_text_here"
32 # 2017-04-01, Sébastien Helleu <flashcode@flashtux.org>:
33 # version 2.5: add option "buffer_number"
34 # 2017-03-02, Sébastien Helleu <flashcode@flashtux.org>:
35 # version 2.4: fix syntax and indentation error
36 # 2017-02-25, Simmo Saan <simmo.saan@gmail.com>
37 # version 2.3: fix fuzzy search breaking buffer number search display
38 # 2016-01-28, ylambda <ylambda@koalabeast.com>
39 # version 2.2: add option "fuzzy_search"
40 # 2015-11-12, nils_2 <weechatter@arcor.de>
41 # version 2.1: fix problem with buffer short_name "weechat", using option
42 # "use_core_instead_weechat", see:
43 # https://github.com/weechat/weechat/issues/574
44 # 2014-05-12, Sébastien Helleu <flashcode@flashtux.org>:
45 # version 2.0: add help on options, replace option "sort_by_activity" by
46 # "sort" (add sort by name and first match at beginning of
47 # name and by number), PEP8 compliance
48 # 2012-11-26, Nei <anti.teamidiot.de>
49 # version 1.9: add auto_jump option to automatically go to buffer when it
50 # is uniquely selected
51 # 2012-09-17, Sébastien Helleu <flashcode@flashtux.org>:
52 # version 1.8: fix jump to non-active merged buffers (jump with buffer name
53 # instead of number)
54 # 2012-01-03 nils_2 <weechatter@arcor.de>
55 # version 1.7: add option "use_core_instead_weechat"
56 # 2012-01-03, Sébastien Helleu <flashcode@flashtux.org>:
57 # version 1.6: make script compatible with Python 3.x
58 # 2011-08-24, stfn <stfnmd@googlemail.com>:
59 # version 1.5: /go with name argument jumps directly to buffer
60 # Remember cursor position in buffer input
61 # 2011-05-31, Elián Hanisch <lambdae2@gmail.com>:
62 # version 1.4: Sort list of buffers by activity.
63 # 2011-04-25, Sébastien Helleu <flashcode@flashtux.org>:
64 # version 1.3: add info "go_running" (used by script input_lock.rb)
65 # 2010-11-01, Sébastien Helleu <flashcode@flashtux.org>:
66 # version 1.2: use high priority for hooks to prevent conflict with other
67 # plugins/scripts (WeeChat >= 0.3.4 only)
68 # 2010-03-25, Elián Hanisch <lambdae2@gmail.com>:
69 # version 1.1: use a space to match the end of a string
70 # 2009-11-16, Sébastien Helleu <flashcode@flashtux.org>:
71 # version 1.0: add new option to display short names
72 # 2009-06-15, Sébastien Helleu <flashcode@flashtux.org>:
73 # version 0.9: fix typo in /help go with command /key
74 # 2009-05-16, Sébastien Helleu <flashcode@flashtux.org>:
75 # version 0.8: search buffer by number, fix bug when window is split
76 # 2009-05-03, Sébastien Helleu <flashcode@flashtux.org>:
77 # version 0.7: eat tab key (do not complete input, just move buffer
78 # pointer)
79 # 2009-05-02, Sébastien Helleu <flashcode@flashtux.org>:
80 # version 0.6: sync with last API changes
81 # 2009-03-22, Sébastien Helleu <flashcode@flashtux.org>:
82 # version 0.5: update modifier signal name for input text display,
83 # fix arguments for function string_remove_color
84 # 2009-02-18, Sébastien Helleu <flashcode@flashtux.org>:
85 # version 0.4: do not hook command and init options if register failed
86 # 2009-02-08, Sébastien Helleu <flashcode@flashtux.org>:
87 # version 0.3: case insensitive search for buffers names
88 # 2009-02-08, Sébastien Helleu <flashcode@flashtux.org>:
89 # version 0.2: add help about Tab key
90 # 2009-02-08, Sébastien Helleu <flashcode@flashtux.org>:
91 # version 0.1: initial release
92 #
93
94 """
95 Quick jump to buffers.
96 (this script requires WeeChat 0.3.0 or newer)
97 """
98
99 from __future__ import print_function
100
101 SCRIPT_NAME = 'go'
102 SCRIPT_AUTHOR = 'Sébastien Helleu <flashcode@flashtux.org>'
103 SCRIPT_VERSION = '2.9'
104 SCRIPT_LICENSE = 'GPL3'
105 SCRIPT_DESC = 'Quick jump to buffers'
106
107 SCRIPT_COMMAND = 'go'
108
109 IMPORT_OK = True
110
111 try:
112 import weechat
113 except ImportError:
114 print('This script must be run under WeeChat.')
115 print('Get WeeChat now at: https://weechat.org/')
116 IMPORT_OK = False
117
118 import re
119
120 # script options
121 SETTINGS = {
122 'auto_jump': (
123 'off',
124 'automatically jump to buffer when it is uniquely selected'),
125 'buffer_number': (
126 'on',
127 'display buffer number'),
128 'color_number': (
129 'yellow,magenta',
130 'color for buffer number (not selected)'),
131 'color_number_selected': (
132 'yellow,red',
133 'color for selected buffer number'),
134 'color_name': (
135 'black,cyan',
136 'color for buffer name (not selected)'),
137 'color_name_selected': (
138 'black,brown',
139 'color for a selected buffer name'),
140 'color_name_highlight': (
141 'red,cyan',
142 'color for highlight in buffer name (not selected)'),
143 'color_name_highlight_selected': (
144 'red,brown',
145 'color for highlight in a selected buffer name'),
146 'fuzzy_search': (
147 'off',
148 'search buffer matches using approximation'),
149 'message': (
150 'Go to: ',
151 'message to display before list of buffers'),
152 'min_chars': (
153 '0',
154 'Minimum chars to search and display list of matching buffers'),
155 'short_name': (
156 'off',
157 'display and search in short names instead of buffer name'),
158 'short_name_server': (
159 'off',
160 'prefix short names with server names for search and display'),
161 'sort': (
162 'number,beginning',
163 'comma-separated list of keys to sort buffers '
164 '(the order is important, sorts are performed in the given order): '
165 'name = sort by name (or short name), ',
166 'hotlist = sort by hotlist order, '
167 'number = first match a buffer number before digits in name, '
168 'beginning = first match at beginning of names (or short names); '
169 'the default sort of buffers is by numbers'),
170 'use_core_instead_weechat': (
171 'off',
172 'use name "core" instead of "weechat" for core buffer'),
173 }
174
175 # hooks management
176 HOOK_COMMAND_RUN = {
177 'input': ('/input *', 'go_command_run_input'),
178 'buffer': ('/buffer *', 'go_command_run_buffer'),
179 'window': ('/window *', 'go_command_run_window'),
180 }
181 hooks = {}
182
183 # input before command /go (we'll restore it later)
184 saved_input = ''
185 saved_input_pos = 0
186
187 # last user input (if changed, we'll update list of matching buffers)
188 old_input = None
189
190 # matching buffers
191 buffers = []
192 buffers_pos = 0
193
194
195 def go_option_enabled(option):
196 """Checks if a boolean script option is enabled or not."""
197 return weechat.config_string_to_boolean(weechat.config_get_plugin(option))
198
199
200 def go_info_running(data, info_name, arguments):
201 """Returns "1" if go is running, otherwise "0"."""
202 return '1' if 'modifier' in hooks else '0'
203
204
205 def go_unhook_one(hook):
206 """Unhook something hooked by this script."""
207 global hooks
208 if hook in hooks:
209 weechat.unhook(hooks[hook])
210 del hooks[hook]
211
212
213 def go_unhook_all():
214 """Unhook all."""
215 go_unhook_one('modifier')
216 for hook in HOOK_COMMAND_RUN:
217 go_unhook_one(hook)
218
219
220 def go_hook_all():
221 """Hook command_run and modifier."""
222 global hooks
223 priority = ''
224 version = weechat.info_get('version_number', '') or 0
225 # use high priority for hook to prevent conflict with other plugins/scripts
226 # (WeeChat >= 0.3.4 only)
227 if int(version) >= 0x00030400:
228 priority = '2000|'
229 for hook, value in HOOK_COMMAND_RUN.items():
230 if hook not in hooks:
231 hooks[hook] = weechat.hook_command_run(
232 '%s%s' % (priority, value[0]),
233 value[1], '')
234 if 'modifier' not in hooks:
235 hooks['modifier'] = weechat.hook_modifier(
236 'input_text_display_with_cursor', 'go_input_modifier', '')
237
238
239 def go_start(buf):
240 """Start go on buffer."""
241 global saved_input, saved_input_pos, old_input, buffers_pos
242 go_hook_all()
243 saved_input = weechat.buffer_get_string(buf, 'input')
244 saved_input_pos = weechat.buffer_get_integer(buf, 'input_pos')
245 weechat.buffer_set(buf, 'input', '')
246 old_input = None
247 buffers_pos = 0
248
249
250 def go_end(buf):
251 """End go on buffer."""
252 global saved_input, saved_input_pos, old_input
253 go_unhook_all()
254 weechat.buffer_set(buf, 'input', saved_input)
255 weechat.buffer_set(buf, 'input_pos', str(saved_input_pos))
256 old_input = None
257
258
259 def go_match_beginning(buf, string):
260 """Check if a string matches the beginning of buffer name/short name."""
261 if not string:
262 return False
263 esc_str = re.escape(string)
264 if re.search(r'^#?' + esc_str, buf['name']) \
265 or re.search(r'^#?' + esc_str, buf['short_name']):
266 return True
267 return False
268
269
270 def go_match_fuzzy(name, string):
271 """Check if string matches name using approximation."""
272 if not string:
273 return False
274
275 name_len = len(name)
276 string_len = len(string)
277
278 if string_len > name_len:
279 return False
280 if name_len == string_len:
281 return name == string
282
283 # Attempt to match all chars somewhere in name
284 prev_index = -1
285 for i, char in enumerate(string):
286 index = name.find(char, prev_index+1)
287 if index == -1:
288 return False
289 prev_index = index
290 return True
291
292
293 def go_now(buf, args):
294 """Go to buffer specified by args."""
295 listbuf = go_matching_buffers(args)
296 if not listbuf:
297 return
298
299 # prefer buffer that matches at beginning (if option is enabled)
300 if 'beginning' in weechat.config_get_plugin('sort').split(','):
301 for index in range(len(listbuf)):
302 if go_match_beginning(listbuf[index], args):
303 weechat.command(buf,
304 '/buffer ' + str(listbuf[index]['full_name']))
305 return
306
307 # jump to first buffer in matching buffers by default
308 weechat.command(buf, '/buffer ' + str(listbuf[0]['full_name']))
309
310
311 def go_cmd(data, buf, args):
312 """Command "/go": just hook what we need."""
313 global hooks
314 if args:
315 go_now(buf, args)
316 elif 'modifier' in hooks:
317 go_end(buf)
318 else:
319 go_start(buf)
320 return weechat.WEECHAT_RC_OK
321
322
323 def go_matching_buffers(strinput):
324 """Return a list with buffers matching user input."""
325 global buffers_pos
326 listbuf = []
327 if len(strinput) == 0:
328 buffers_pos = 0
329 strinput = strinput.lower()
330 infolist = weechat.infolist_get('buffer', '', '')
331 while weechat.infolist_next(infolist):
332 pointer = weechat.infolist_pointer(infolist, 'pointer')
333 short_name = weechat.infolist_string(infolist, 'short_name')
334 server = weechat.buffer_get_string(pointer, 'localvar_server')
335 if go_option_enabled('short_name'):
336 if go_option_enabled('short_name_server') and server:
337 name = server + '.' + short_name
338 else:
339 name = short_name
340 else:
341 name = weechat.infolist_string(infolist, 'name')
342 if name == 'weechat' \
343 and go_option_enabled('use_core_instead_weechat') \
344 and weechat.infolist_string(infolist, 'plugin_name') == 'core':
345 name = 'core'
346 number = weechat.infolist_integer(infolist, 'number')
347 full_name = weechat.infolist_string(infolist, 'full_name')
348 if not full_name:
349 full_name = '%s.%s' % (
350 weechat.infolist_string(infolist, 'plugin_name'),
351 weechat.infolist_string(infolist, 'name'))
352 matching = name.lower().find(strinput) >= 0
353 if not matching and strinput[-1] == ' ':
354 matching = name.lower().endswith(strinput.strip())
355 if not matching and go_option_enabled('fuzzy_search'):
356 matching = go_match_fuzzy(name.lower(), strinput)
357 if not matching and strinput.isdigit():
358 matching = str(number).startswith(strinput)
359 if len(strinput) == 0 or matching:
360 listbuf.append({
361 'number': number,
362 'short_name': short_name,
363 'name': name,
364 'full_name': full_name,
365 'pointer': pointer,
366 })
367 weechat.infolist_free(infolist)
368
369 # sort buffers
370 hotlist = []
371 infolist = weechat.infolist_get('hotlist', '', '')
372 while weechat.infolist_next(infolist):
373 hotlist.append(
374 weechat.infolist_pointer(infolist, 'buffer_pointer'))
375 weechat.infolist_free(infolist)
376 last_index_hotlist = len(hotlist)
377
378 def _sort_name(buf):
379 """Sort buffers by name (or short name)."""
380 return buf['name']
381
382 def _sort_hotlist(buf):
383 """Sort buffers by hotlist order."""
384 try:
385 return hotlist.index(buf['pointer'])
386 except ValueError:
387 # not in hotlist, always last.
388 return last_index_hotlist
389
390 def _sort_match_number(buf):
391 """Sort buffers by match on number."""
392 return 0 if str(buf['number']) == strinput else 1
393
394 def _sort_match_beginning(buf):
395 """Sort buffers by match at beginning."""
396 return 0 if go_match_beginning(buf, strinput) else 1
397
398 funcs = {
399 'name': _sort_name,
400 'hotlist': _sort_hotlist,
401 'number': _sort_match_number,
402 'beginning': _sort_match_beginning,
403 }
404
405 for key in weechat.config_get_plugin('sort').split(','):
406 if key in funcs:
407 listbuf = sorted(listbuf, key=funcs[key])
408
409 if not strinput:
410 index = [i for i, buf in enumerate(listbuf)
411 if buf['pointer'] == weechat.current_buffer()]
412 if index:
413 buffers_pos = index[0]
414
415 return listbuf
416
417
418 def go_buffers_to_string(listbuf, pos, strinput):
419 """Return string built with list of buffers found (matching user input)."""
420 try:
421 if len(strinput) < int(weechat.config_get_plugin('min_chars')):
422 return ''
423 except:
424 pass
425 string = ''
426 strinput = strinput.lower()
427 for i in range(len(listbuf)):
428 selected = '_selected' if i == pos else ''
429 buffer_name = listbuf[i]['name']
430 index = buffer_name.lower().find(strinput)
431 if index >= 0:
432 index2 = index + len(strinput)
433 name = '%s%s%s%s%s' % (
434 buffer_name[:index],
435 weechat.color(weechat.config_get_plugin(
436 'color_name_highlight' + selected)),
437 buffer_name[index:index2],
438 weechat.color(weechat.config_get_plugin(
439 'color_name' + selected)),
440 buffer_name[index2:])
441 elif go_option_enabled("fuzzy_search") and \
442 go_match_fuzzy(buffer_name.lower(), strinput):
443 name = ""
444 prev_index = -1
445 for char in strinput.lower():
446 index = buffer_name.lower().find(char, prev_index+1)
447 if prev_index < 0:
448 name += buffer_name[:index]
449 name += weechat.color(weechat.config_get_plugin(
450 'color_name_highlight' + selected))
451 if prev_index >= 0 and index > prev_index+1:
452 name += weechat.color(weechat.config_get_plugin(
453 'color_name' + selected))
454 name += buffer_name[prev_index+1:index]
455 name += weechat.color(weechat.config_get_plugin(
456 'color_name_highlight' + selected))
457 name += buffer_name[index]
458 prev_index = index
459
460 name += weechat.color(weechat.config_get_plugin(
461 'color_name' + selected))
462 name += buffer_name[prev_index+1:]
463 else:
464 name = buffer_name
465 string += ' '
466 if go_option_enabled('buffer_number'):
467 string += '%s%s' % (
468 weechat.color(weechat.config_get_plugin(
469 'color_number' + selected)),
470 str(listbuf[i]['number']))
471 string += '%s%s%s' % (
472 weechat.color(weechat.config_get_plugin(
473 'color_name' + selected)),
474 name,
475 weechat.color('reset'))
476 return ' ' + string if string else ''
477
478
479 def go_input_modifier(data, modifier, modifier_data, string):
480 """This modifier is called when input text item is built by WeeChat.
481
482 This is commonly called after changes in input or cursor move: it builds
483 a new input with prefix ("Go to:"), and suffix (list of buffers found).
484 """
485 global old_input, buffers, buffers_pos
486 if modifier_data != weechat.current_buffer():
487 return ''
488 names = ''
489 new_input = weechat.string_remove_color(string, '')
490 new_input = new_input.lstrip()
491 if old_input is None or new_input != old_input:
492 old_buffers = buffers
493 buffers = go_matching_buffers(new_input)
494 if buffers != old_buffers and len(new_input) > 0:
495 if len(buffers) == 1 and go_option_enabled('auto_jump'):
496 weechat.command(modifier_data, '/wait 1ms /input return')
497 buffers_pos = 0
498 old_input = new_input
499 names = go_buffers_to_string(buffers, buffers_pos, new_input.strip())
500 return weechat.config_get_plugin('message') + string + names
501
502
503 def go_command_run_input(data, buf, command):
504 """Function called when a command "/input xxx" is run."""
505 global buffers, buffers_pos
506 if command.startswith('/input search_text') or command.startswith('/input jump'):
507 # search text or jump to another buffer is forbidden now
508 return weechat.WEECHAT_RC_OK_EAT
509 elif command == '/input complete_next':
510 # choose next buffer in list
511 buffers_pos += 1
512 if buffers_pos >= len(buffers):
513 buffers_pos = 0
514 weechat.hook_signal_send('input_text_changed',
515 weechat.WEECHAT_HOOK_SIGNAL_POINTER, buf)
516 return weechat.WEECHAT_RC_OK_EAT
517 elif command == '/input complete_previous':
518 # choose previous buffer in list
519 buffers_pos -= 1
520 if buffers_pos < 0:
521 buffers_pos = len(buffers) - 1
522 weechat.hook_signal_send('input_text_changed',
523 weechat.WEECHAT_HOOK_SIGNAL_POINTER, buf)
524 return weechat.WEECHAT_RC_OK_EAT
525 elif command == '/input return':
526 # switch to selected buffer (if any)
527 go_end(buf)
528 if len(buffers) > 0:
529 weechat.command(
530 buf, '/buffer ' + str(buffers[buffers_pos]['full_name']))
531 return weechat.WEECHAT_RC_OK_EAT
532 return weechat.WEECHAT_RC_OK
533
534
535 def go_command_run_buffer(data, buf, command):
536 """Function called when a command "/buffer xxx" is run."""
537 return weechat.WEECHAT_RC_OK_EAT
538
539
540 def go_command_run_window(data, buf, command):
541 """Function called when a command "/window xxx" is run."""
542 return weechat.WEECHAT_RC_OK_EAT
543
544
545 def go_unload_script():
546 """Function called when script is unloaded."""
547 go_unhook_all()
548 return weechat.WEECHAT_RC_OK
549
550
551 def go_main():
552 """Entry point."""
553 if not weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION,
554 SCRIPT_LICENSE, SCRIPT_DESC,
555 'go_unload_script', ''):
556 return
557 weechat.hook_command(
558 SCRIPT_COMMAND,
559 'Quick jump to buffers', '[name]',
560 'name: directly jump to buffer by name (without argument, list is '
561 'displayed)\n\n'
562 'You can bind command to a key, for example:\n'
563 ' /key bind meta-g /go\n\n'
564 'You can use completion key (commonly Tab and shift-Tab) to select '
565 'next/previous buffer in list.',
566 '%(buffers_names)',
567 'go_cmd', '')
568
569 # set default settings
570 version = weechat.info_get('version_number', '') or 0
571 for option, value in SETTINGS.items():
572 if not weechat.config_is_set_plugin(option):
573 weechat.config_set_plugin(option, value[0])
574 if int(version) >= 0x00030500:
575 weechat.config_set_desc_plugin(
576 option, '%s (default: "%s")' % (value[1], value[0]))
577 weechat.hook_info('go_running',
578 'Return "1" if go is running, otherwise "0"',
579 '',
580 'go_info_running', '')
581
582
583 if __name__ == "__main__" and IMPORT_OK:
584 go_main()