# # SPDX-FileCopyrightText: 2009-2026 Sébastien Helleu # SPDX-FileCopyrightText: 2010 m4v # SPDX-FileCopyrightText: 2011 stfn # SPDX-FileCopyrightText: 2025 Leo Vivier # # SPDX-License-Identifier: GPL-3.0-or-later # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # """Quick jump to buffers.""" import re from typing import Any SCRIPT_NAME = "go" SCRIPT_AUTHOR = "Sébastien Helleu " SCRIPT_VERSION = "3.1.1" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Quick jump to buffers" SCRIPT_COMMAND = "go" IMPORT_OK = True try: import weechat # type: ignore[import] except ImportError: print("This script must be run under WeeChat.") print("Get WeeChat now at: https://weechat.org/") IMPORT_OK = False # script options SETTINGS = { "auto_jump": ("off", "automatically jump to buffer when it is uniquely selected"), "buffer_number": ("on", "display buffer number"), "color_number": ("yellow,magenta", "color for buffer number (not selected)"), "color_number_selected": ("yellow,red", "color for selected buffer number"), "color_name": ("black,cyan", "color for buffer name (not selected)"), "color_name_selected": ("black,brown", "color for a selected buffer name"), "color_name_highlight": ( "red,cyan", "color for highlight in buffer name (not selected)", ), "color_name_highlight_selected": ( "red,brown", "color for highlight in a selected buffer name", ), "regexp_search": ("off", "search buffer matches using regexps"), "fuzzy_search": ("off", "search buffer matches using approximation"), "message": ("Go to: ", "message to display before list of buffers"), "min_chars": ("0", "Minimum chars to search and display list of matching buffers"), "short_name": ("off", "display and search in short names instead of buffer name"), "short_name_server": ( "off", "prefix short names with server names for search and display", ), "sort": ( "number,beginning", "comma-separated list of keys to sort buffers " "(the order is important, sorts are performed in the given order): " "name = sort by name (or short name), ", "hotlist = sort by hotlist order, " "number = first match a buffer number before digits in name, " "beginning = first match at beginning of names (or short names); " "the default sort of buffers is by numbers", ), "use_core_instead_weechat": ( "off", 'use name "core" instead of "weechat" for core buffer', ), } # hooks management HOOK_COMMAND_RUN = { "input": ("/input *", "go_command_run_input"), "buffer": ("/buffer *", "go_command_run_buffer"), "window": ("/window *", "go_command_run_window"), } hooks = {} # input before command /go (we'll restore it later) saved_input = "" saved_input_pos = 0 # last user input (if changed, we'll update list of matching buffers) old_input = None # matching buffers buffers = [] buffers_pos = 0 def go_option_enabled(option: str) -> bool: """Check if a boolean script option is enabled or not.""" return weechat.config_string_to_boolean(weechat.config_get_plugin(option)) def go_info_running(_data: str, _info_name: str, _arguments: str) -> str: """Return "1" if go is running, otherwise "0".""" return "1" if "modifier" in hooks else "0" def go_unhook_one(hook: str) -> None: """Unhook something hooked by this script.""" global hooks if hook in hooks: weechat.unhook(hooks[hook]) del hooks[hook] def go_unhook_all() -> None: """Unhook all.""" go_unhook_one("modifier") for hook in HOOK_COMMAND_RUN: go_unhook_one(hook) weechat.bar_item_update("input_text") def go_hook_all() -> None: """Hook command_run and modifier.""" global hooks priority = "" version = weechat.info_get("version_number", "") or 0 # use high priority for hook to prevent conflict with other plugins/scripts # (WeeChat >= 0.3.4 only) if int(version) >= 0x00030400: priority = "2000|" for hook, value in HOOK_COMMAND_RUN.items(): if hook not in hooks: hooks[hook] = weechat.hook_command_run( "%s%s" % (priority, value[0]), value[1], "" ) if "modifier" not in hooks: hooks["modifier"] = weechat.hook_modifier( "input_text_display_with_cursor", "go_input_modifier", "" ) weechat.bar_item_update("input_text") def go_start(buf: str) -> None: """Start go on buffer.""" global saved_input, saved_input_pos, old_input, buffers_pos go_hook_all() saved_input = weechat.buffer_get_string(buf, "input") saved_input_pos = weechat.buffer_get_integer(buf, "input_pos") weechat.buffer_set(buf, "input", "") old_input = None buffers_pos = 0 def go_end(buf: str) -> None: """End go on buffer.""" global saved_input, saved_input_pos, old_input go_unhook_all() weechat.buffer_set(buf, "input", saved_input) weechat.buffer_set(buf, "input_pos", str(saved_input_pos)) old_input = None def go_match_beginning(buf: str, string: str) -> bool: """Check if a string matches the beginning of buffer name/short name.""" if not string: return False esc_str = re.escape(string) if re.search(r"^#?" + esc_str, buf["name"]) or re.search( r"^#?" + esc_str, buf["short_name"] ): return True return False def go_match_fuzzy(name: str, string: str) -> bool: """Check if string matches name using approximation.""" if not string: return False name_len = len(name) string_len = len(string) if string_len > name_len: return False if name_len == string_len: return name == string # Attempt to match all chars somewhere in name prev_index = -1 for i, char in enumerate(string): index = name.find(char, prev_index + 1) if index == -1: return False prev_index = index return True def go_now(buf: str, args: str) -> None: """Go to buffer specified by args.""" listbuf = go_matching_buffers(args) if not listbuf: return # prefer buffer that matches at beginning (if option is enabled) if "beginning" in weechat.config_get_plugin("sort").split(","): for index in range(len(listbuf)): if go_match_beginning(listbuf[index], args): weechat.command(buf, "/buffer " + str(listbuf[index]["full_name"])) return # jump to first buffer in matching buffers by default weechat.command(buf, "/buffer " + str(listbuf[0]["full_name"])) def go_cmd(_data: str, buf: str, args: str) -> int: """Command "/go": just hook what we need.""" global hooks if args: go_now(buf, args) elif "modifier" in hooks: go_end(buf) else: go_start(buf) return weechat.WEECHAT_RC_OK def go_matching_buffers(strinput: str) -> list[dict[str, Any]]: """Return a list with buffers matching user input.""" global buffers_pos listbuf = [] if len(strinput) == 0: buffers_pos = 0 strinput = strinput.lower() infolist = weechat.infolist_get("buffer", "", "") while weechat.infolist_next(infolist): pointer = weechat.infolist_pointer(infolist, "pointer") short_name = weechat.infolist_string(infolist, "short_name") server = weechat.buffer_get_string(pointer, "localvar_server") if go_option_enabled("short_name"): if go_option_enabled("short_name_server") and server: name = server + "." + short_name else: name = short_name else: name = weechat.infolist_string(infolist, "name") if ( name == "weechat" and go_option_enabled("use_core_instead_weechat") and weechat.infolist_string(infolist, "plugin_name") == "core" ): name = "core" number = weechat.infolist_integer(infolist, "number") full_name = weechat.infolist_string(infolist, "full_name") if not full_name: full_name = "%s.%s" % ( weechat.infolist_string(infolist, "plugin_name"), weechat.infolist_string(infolist, "name"), ) if go_option_enabled("regexp_search"): matching = bool(re.search(strinput, name, re.IGNORECASE)) else: matching = name.lower().find(strinput) >= 0 if not matching and strinput[-1] == " ": matching = name.lower().endswith(strinput.strip()) if not matching and go_option_enabled("fuzzy_search"): matching = go_match_fuzzy(name.lower(), strinput) if not matching and strinput.isdigit(): matching = str(number).startswith(strinput) if len(strinput) == 0 or matching: listbuf.append( { "number": number, "short_name": short_name, "name": name, "full_name": full_name, "pointer": pointer, } ) weechat.infolist_free(infolist) # sort buffers hotlist = [] infolist = weechat.infolist_get("hotlist", "", "") while weechat.infolist_next(infolist): hotlist.append(weechat.infolist_pointer(infolist, "buffer_pointer")) weechat.infolist_free(infolist) last_index_hotlist = len(hotlist) def _sort_name(buf): """Sort buffers by name (or short name).""" return buf["name"] def _sort_hotlist(buf): """Sort buffers by hotlist order.""" try: return hotlist.index(buf["pointer"]) except ValueError: # not in hotlist, always last. return last_index_hotlist def _sort_match_number(buf): """Sort buffers by match on number.""" return 0 if str(buf["number"]) == strinput else 1 def _sort_match_beginning(buf): """Sort buffers by match at beginning.""" return 0 if go_match_beginning(buf, strinput) else 1 funcs = { "name": _sort_name, "hotlist": _sort_hotlist, "number": _sort_match_number, "beginning": _sort_match_beginning, } for key in weechat.config_get_plugin("sort").split(","): if key in funcs: listbuf = sorted(listbuf, key=funcs[key]) if not strinput: index = [ i for i, buf in enumerate(listbuf) if buf["pointer"] == weechat.current_buffer() ] if index: buffers_pos = index[0] return listbuf def go_buffers_to_string(listbuf: list[dict[str, Any]], pos: int, strinput: str) -> str: """Return string built with list of buffers found (matching user input).""" try: if len(strinput) < int(weechat.config_get_plugin("min_chars")): return "" except ValueError: pass string = "" strinput = strinput.lower() for i in range(len(listbuf)): selected = "_selected" if i == pos else "" buffer_name = listbuf[i]["name"] index = buffer_name.lower().find(strinput) if index >= 0: index2 = index + len(strinput) name = "%s%s%s%s%s" % ( buffer_name[:index], weechat.color( weechat.config_get_plugin("color_name_highlight" + selected) ), buffer_name[index:index2], weechat.color(weechat.config_get_plugin("color_name" + selected)), buffer_name[index2:], ) elif go_option_enabled("fuzzy_search") and go_match_fuzzy( buffer_name.lower(), strinput ): name = "" prev_index = -1 for char in strinput.lower(): index = buffer_name.lower().find(char, prev_index + 1) if prev_index < 0: name += buffer_name[:index] name += weechat.color( weechat.config_get_plugin("color_name_highlight" + selected) ) if prev_index >= 0 and index > prev_index + 1: name += weechat.color( weechat.config_get_plugin("color_name" + selected) ) name += buffer_name[prev_index + 1 : index] name += weechat.color( weechat.config_get_plugin("color_name_highlight" + selected) ) name += buffer_name[index] prev_index = index name += weechat.color(weechat.config_get_plugin("color_name" + selected)) name += buffer_name[prev_index + 1 :] else: name = buffer_name string += " " if go_option_enabled("buffer_number"): string += "%s%s" % ( weechat.color(weechat.config_get_plugin("color_number" + selected)), str(listbuf[i]["number"]), ) string += "%s%s%s" % ( weechat.color(weechat.config_get_plugin("color_name" + selected)), name, weechat.color("reset"), ) return " " + string if string else "" def go_input_modifier(_data: str, _modifier: str, modifier_data: str, string: str) -> str: """This modifier is called when input text item is built by WeeChat. This is commonly called after changes in input or cursor move: it builds a new input with prefix ("Go to:"), and suffix (list of buffers found). """ global old_input, buffers, buffers_pos if modifier_data != weechat.current_buffer(): return "" names = "" new_input = weechat.string_remove_color(string, "") new_input = new_input.lstrip() if old_input is None or new_input != old_input: old_buffers = buffers buffers = go_matching_buffers(new_input) if buffers != old_buffers and len(new_input) > 0: if len(buffers) == 1 and go_option_enabled("auto_jump"): weechat.command(modifier_data, "/wait 1ms /input return") buffers_pos = 0 old_input = new_input names = go_buffers_to_string(buffers, buffers_pos, new_input.strip()) return weechat.config_get_plugin("message") + string + names def go_command_run_input(_data: str, buf: str, command: str) -> int: """Function called when a command "/input xxx" is run.""" global buffers, buffers_pos if command.startswith("/input search_text") or command.startswith("/input jump"): # search text or jump to another buffer is forbidden now return weechat.WEECHAT_RC_OK_EAT elif command == "/input complete_next": # choose next buffer in list buffers_pos += 1 if buffers_pos >= len(buffers): buffers_pos = 0 weechat.hook_signal_send( "input_text_changed", weechat.WEECHAT_HOOK_SIGNAL_POINTER, buf ) return weechat.WEECHAT_RC_OK_EAT elif command == "/input complete_previous": # choose previous buffer in list buffers_pos -= 1 if buffers_pos < 0: buffers_pos = len(buffers) - 1 weechat.hook_signal_send( "input_text_changed", weechat.WEECHAT_HOOK_SIGNAL_POINTER, buf ) return weechat.WEECHAT_RC_OK_EAT elif command == "/input return": # switch to selected buffer (if any) go_end(buf) if len(buffers) > 0: weechat.command(buf, "/buffer " + str(buffers[buffers_pos]["full_name"])) return weechat.WEECHAT_RC_OK_EAT return weechat.WEECHAT_RC_OK def go_command_run_buffer(_data: str, _buf: str, _command: str) -> int: """Function called when a command "/buffer xxx" is run.""" return weechat.WEECHAT_RC_OK_EAT def go_command_run_window(_data: str, buf: str, command: str) -> int: """Function called when a command "/window xxx" is run.""" if command == "/window scroll_bottom": # cancel selection and return to input go_end(buf) return weechat.WEECHAT_RC_OK_EAT return weechat.WEECHAT_RC_OK_EAT def go_unload_script() -> int: """Function called when script is unloaded.""" go_unhook_all() return weechat.WEECHAT_RC_OK def go_main() -> None: """Entry point.""" if not weechat.register( SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "go_unload_script", "", ): return weechat.hook_command( SCRIPT_COMMAND, "Quick jump to buffers", "[name]", "name: directly jump to buffer by name (without argument, list is " "displayed)\n\n" "You can bind command to a key, for example:\n" " /key bind meta-g /go\n\n" "You can use completion key (commonly Tab and shift-Tab) to select " "next/previous buffer in list.", "%(buffers_names)", "go_cmd", "", ) # set default settings version = weechat.info_get("version_number", "") or 0 for option, value in SETTINGS.items(): if not weechat.config_is_set_plugin(option): weechat.config_set_plugin(option, value[0]) if int(version) >= 0x00030500: weechat.config_set_desc_plugin( option, '%s (default: "%s")' % (value[1], value[0]) ) weechat.hook_info( "go_running", 'Return "1" if go is running, otherwise "0"', "", "go_info_running", "", ) if __name__ == "__main__" and IMPORT_OK: go_main()