1 # -*- coding: utf-8 -*-
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>
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.
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.
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/>.
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
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
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
95 Quick jump to buffers.
96 (this script requires WeeChat 0.3.0 or newer)
99 from __future__
import print_function
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'
107 SCRIPT_COMMAND
= 'go'
114 print('This script must be run under WeeChat.')
115 print('Get WeeChat now at: https://weechat.org/')
124 'automatically jump to buffer when it is uniquely selected'),
127 'display buffer number'),
130 'color for buffer number (not selected)'),
131 'color_number_selected': (
133 'color for selected buffer number'),
136 'color for buffer name (not selected)'),
137 'color_name_selected': (
139 'color for a selected buffer name'),
140 'color_name_highlight': (
142 'color for highlight in buffer name (not selected)'),
143 'color_name_highlight_selected': (
145 'color for highlight in a selected buffer name'),
148 'search buffer matches using approximation'),
151 'message to display before list of buffers'),
154 'Minimum chars to search and display list of matching buffers'),
157 'display and search in short names instead of buffer name'),
158 'short_name_server': (
160 'prefix short names with server names for search and display'),
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': (
172 'use name "core" instead of "weechat" for core buffer'),
177 'input': ('/input *', 'go_command_run_input'),
178 'buffer': ('/buffer *', 'go_command_run_buffer'),
179 'window': ('/window *', 'go_command_run_window'),
183 # input before command /go (we'll restore it later)
187 # last user input (if changed, we'll update list of matching buffers)
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
))
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'
205 def go_unhook_one(hook
):
206 """Unhook something hooked by this script."""
209 weechat
.unhook(hooks
[hook
])
215 go_unhook_one('modifier')
216 for hook
in HOOK_COMMAND_RUN
:
221 """Hook command_run and modifier."""
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:
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]),
234 if 'modifier' not in hooks
:
235 hooks
['modifier'] = weechat
.hook_modifier(
236 'input_text_display_with_cursor', 'go_input_modifier', '')
240 """Start go on buffer."""
241 global saved_input
, saved_input_pos
, old_input
, buffers_pos
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', '')
251 """End go on buffer."""
252 global saved_input
, saved_input_pos
, old_input
254 weechat
.buffer_set(buf
, 'input', saved_input
)
255 weechat
.buffer_set(buf
, 'input_pos', str(saved_input_pos
))
259 def go_match_beginning(buf
, string
):
260 """Check if a string matches the beginning of buffer name/short name."""
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']):
270 def go_match_fuzzy(name
, string
):
271 """Check if string matches name using approximation."""
276 string_len
= len(string
)
278 if string_len
> name_len
:
280 if name_len
== string_len
:
281 return name
== string
283 # Attempt to match all chars somewhere in name
285 for i
, char
in enumerate(string
):
286 index
= name
.find(char
, prev_index
+1)
293 def go_now(buf
, args
):
294 """Go to buffer specified by args."""
295 listbuf
= go_matching_buffers(args
)
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
):
304 '/buffer ' + str(listbuf
[index
]['full_name']))
307 # jump to first buffer in matching buffers by default
308 weechat
.command(buf
, '/buffer ' + str(listbuf
[0]['full_name']))
311 def go_cmd(data
, buf
, args
):
312 """Command "/go": just hook what we need."""
316 elif 'modifier' in hooks
:
320 return weechat
.WEECHAT_RC_OK
323 def go_matching_buffers(strinput
):
324 """Return a list with buffers matching user input."""
327 if len(strinput
) == 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
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':
346 number
= weechat
.infolist_integer(infolist
, 'number')
347 full_name
= weechat
.infolist_string(infolist
, '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
:
362 'short_name': short_name
,
364 'full_name': full_name
,
367 weechat
.infolist_free(infolist
)
371 infolist
= weechat
.infolist_get('hotlist', '', '')
372 while weechat
.infolist_next(infolist
):
374 weechat
.infolist_pointer(infolist
, 'buffer_pointer'))
375 weechat
.infolist_free(infolist
)
376 last_index_hotlist
= len(hotlist
)
379 """Sort buffers by name (or short name)."""
382 def _sort_hotlist(buf
):
383 """Sort buffers by hotlist order."""
385 return hotlist
.index(buf
['pointer'])
387 # not in hotlist, always last.
388 return last_index_hotlist
390 def _sort_match_number(buf
):
391 """Sort buffers by match on number."""
392 return 0 if str(buf
['number']) == strinput
else 1
394 def _sort_match_beginning(buf
):
395 """Sort buffers by match at beginning."""
396 return 0 if go_match_beginning(buf
, strinput
) else 1
400 'hotlist': _sort_hotlist
,
401 'number': _sort_match_number
,
402 'beginning': _sort_match_beginning
,
405 for key
in weechat
.config_get_plugin('sort').split(','):
407 listbuf
= sorted(listbuf
, key
=funcs
[key
])
410 index
= [i
for i
, buf
in enumerate(listbuf
)
411 if buf
['pointer'] == weechat
.current_buffer()]
413 buffers_pos
= index
[0]
418 def go_buffers_to_string(listbuf
, pos
, strinput
):
419 """Return string built with list of buffers found (matching user input)."""
421 if len(strinput
) < int(weechat
.config_get_plugin('min_chars')):
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
)
432 index2
= index
+ len(strinput
)
433 name
= '%s%s%s%s%s' % (
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
):
445 for char
in strinput
.lower():
446 index
= buffer_name
.lower().find(char
, prev_index
+1)
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
]
460 name
+= weechat
.color(weechat
.config_get_plugin(
461 'color_name' + selected
))
462 name
+= buffer_name
[prev_index
+1:]
466 if go_option_enabled('buffer_number'):
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
)),
475 weechat
.color('reset'))
476 return ' ' + string
if string
else ''
479 def go_input_modifier(data
, modifier
, modifier_data
, string
):
480 """This modifier is called when input text item is built by WeeChat.
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).
485 global old_input
, buffers
, buffers_pos
486 if modifier_data
!= weechat
.current_buffer():
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')
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
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
512 if buffers_pos
>= len(buffers
):
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
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)
530 buf
, '/buffer ' + str(buffers
[buffers_pos
]['full_name']))
531 return weechat
.WEECHAT_RC_OK_EAT
532 return weechat
.WEECHAT_RC_OK
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
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
545 def go_unload_script():
546 """Function called when script is unloaded."""
548 return weechat
.WEECHAT_RC_OK
553 if not weechat
.register(SCRIPT_NAME
, SCRIPT_AUTHOR
, SCRIPT_VERSION
,
554 SCRIPT_LICENSE
, SCRIPT_DESC
,
555 'go_unload_script', ''):
557 weechat
.hook_command(
559 'Quick jump to buffers', '[name]',
560 'name: directly jump to buffer by name (without argument, list is '
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.',
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"',
580 'go_info_running', '')
583 if __name__
== "__main__" and IMPORT_OK
: