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