my shell and tool configurations
at master 591 lines 22 kB view raw
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""" 100Quick jump to buffers. 101(this script requires WeeChat 0.3.0 or newer) 102""" 103 104from __future__ import print_function 105 106SCRIPT_NAME = 'go' 107SCRIPT_AUTHOR = 'Sébastien Helleu <flashcode@flashtux.org>' 108SCRIPT_VERSION = '3.0.1' 109SCRIPT_LICENSE = 'GPL3' 110SCRIPT_DESC = 'Quick jump to buffers' 111 112SCRIPT_COMMAND = 'go' 113 114IMPORT_OK = True 115 116try: 117 import weechat 118except ImportError: 119 print('This script must be run under WeeChat.') 120 print('Get WeeChat now at: https://weechat.org/') 121 IMPORT_OK = False 122 123import re 124 125# script options 126SETTINGS = { 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 181HOOK_COMMAND_RUN = { 182 'input': ('/input *', 'go_command_run_input'), 183 'buffer': ('/buffer *', 'go_command_run_buffer'), 184 'window': ('/window *', 'go_command_run_window'), 185} 186hooks = {} 187 188# input before command /go (we'll restore it later) 189saved_input = '' 190saved_input_pos = 0 191 192# last user input (if changed, we'll update list of matching buffers) 193old_input = None 194 195# matching buffers 196buffers = [] 197buffers_pos = 0 198 199 200def 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 205def 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 210def 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 218def 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 226def 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 246def 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 257def 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 266def 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 277def 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 300def 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 318def 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 330def 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 425def 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 486def 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 510def 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 542def 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 547def 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 552def go_unload_script(): 553 """Function called when script is unloaded.""" 554 go_unhook_all() 555 return weechat.WEECHAT_RC_OK 556 557 558def 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 590if __name__ == "__main__" and IMPORT_OK: 591 go_main()