diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 21:38:38 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 21:38:38 +0000 |
commit | 2e2851dc13d73352530dd4495c7e05603b2e520d (patch) | |
tree | 622b9cd8e5d32091c9aa9e4937b533975a40356c /deluge/ui/console/modes/cmdline.py | |
parent | Initial commit. (diff) | |
download | deluge-upstream/2.1.2_dev0+20240219.tar.xz deluge-upstream/2.1.2_dev0+20240219.zip |
Adding upstream version 2.1.2~dev0+20240219.upstream/2.1.2_dev0+20240219upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'deluge/ui/console/modes/cmdline.py')
-rw-r--r-- | deluge/ui/console/modes/cmdline.py | 845 |
1 files changed, 845 insertions, 0 deletions
diff --git a/deluge/ui/console/modes/cmdline.py b/deluge/ui/console/modes/cmdline.py new file mode 100644 index 0000000..7b0ff2d --- /dev/null +++ b/deluge/ui/console/modes/cmdline.py @@ -0,0 +1,845 @@ +# +# Copyright (C) 2008-2009 Ido Abramovich <ido.deluge@gmail.com> +# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +import logging +import os +import re + +import deluge.component as component +import deluge.configmanager +from deluge.decorators import overrides +from deluge.ui.console.cmdline.command import Commander +from deluge.ui.console.modes.basemode import BaseMode, move_cursor +from deluge.ui.console.utils import colors +from deluge.ui.console.utils import curses_util as util +from deluge.ui.console.utils.format_utils import ( + delete_alt_backspace, + remove_formatting, + strwidth, +) + +try: + import curses +except ImportError: + pass + +log = logging.getLogger(__name__) +LINES_BUFFER_SIZE = 5000 +INPUT_HISTORY_SIZE = 500 +MAX_HISTFILE_SIZE = 2000 + + +def complete_line(line, possible_matches): + """Find the common prefix of possible matches. + + Proritizing matching-case elements. + """ + + if not possible_matches: + return line + + line = line.replace(r'\ ', ' ') + + matches1 = [] + matches2 = [] + + for match in possible_matches: + match = remove_formatting(match) + match = match.replace(r'\ ', ' ') + m1, m2 = '', '' + for i, c in enumerate(line): + if m1 and m2: + break + if not m1 and c != line[i]: + m1 = line[:i] + if not m2 and c.lower() != line[i].lower(): + m2 = line[:i] + if not m1: + matches1.append(match) + elif not m2: + matches2.append(match) + + possible_matches = matches1 + matches2 + + maxlen = 9001 + + for match in possible_matches[1:]: + for i, c in enumerate(match): + try: + if c.lower() != possible_matches[0][i].lower(): + maxlen = min(maxlen, i) + break + except IndexError: + maxlen = min(maxlen, i) + break + + return possible_matches[0][:maxlen].replace(' ', r'\ ') + + +def commonprefix(m): + """Returns the longest common leading component from list of pathnames.""" + if not m: + return '' + s1 = min(m) + s2 = max(m) + for i, c in enumerate(s1): + if c != s2[i]: + return s1[:i] + return s2 + + +class CmdLine(BaseMode, Commander): + def __init__(self, stdscr, encoding=None): + # Get a handle to the main console + self.console = component.get('ConsoleUI') + Commander.__init__(self, self.console._commands, interactive=True) + + self.batch_write = False + + # A list of strings to be displayed based on the offset (scroll) + self.lines = [] + # The offset to display lines + self.display_lines_offset = 0 + + # Holds the user input and is cleared on 'enter' + self.input = '' + self.input_incomplete = '' + + # Keep track of where the cursor is + self.input_cursor = 0 + # Keep a history of inputs + self.input_history = [] + self.input_history_index = 0 + + # Keep track of double- and multi-tabs + self.tab_count = 0 + + self.console_config = component.get('TorrentList').config + + # To avoid having to truncate the file every time we're writing + # or doing it on exit(and therefore relying on an error-less + # or in other words clean exit, we're going to have two files + # that we swap around based on length + config_dir = deluge.configmanager.get_config_dir() + self.history_file = [ + os.path.join(config_dir, 'cmd_line.hist1'), + os.path.join(config_dir, 'cmd_line.hist2'), + ] + self._hf_lines = [0, 0] + if self.console_config['cmdline']['save_command_history']: + try: + with open(self.history_file[0], encoding='utf8') as _file: + lines1 = _file.read().splitlines() + self._hf_lines[0] = len(lines1) + except OSError: + lines1 = [] + self._hf_lines[0] = 0 + + try: + with open(self.history_file[1], encoding='utf8') as _file: + lines2 = _file.read().splitlines() + self._hf_lines[1] = len(lines2) + except OSError: + lines2 = [] + self._hf_lines[1] = 0 + + # The non-full file is the active one + if self._hf_lines[0] > self._hf_lines[1]: + self.lines = lines1 + lines2 + else: + self.lines = lines2 + lines1 + + if len(self.lines) > MAX_HISTFILE_SIZE: + self.lines = self.lines[-MAX_HISTFILE_SIZE:] + + # Instead of having additional input history file, we can + # simply scan for lines beginning with ">>> " + for i, line in enumerate(self.lines): + line = remove_formatting(line) + if line.startswith('>>> '): + console_input = line[4:] + if self.console_config['cmdline']['ignore_duplicate_lines']: + if len(self.input_history) > 0: + if self.input_history[-1] != console_input: + self.input_history.append(console_input) + else: + self.input_history.append(console_input) + + self.input_history_index = len(self.input_history) + + # show the cursor + util.safe_curs_set(util.Curser.VERY_VISIBLE) + BaseMode.__init__(self, stdscr, encoding, depend=['SessionProxy']) + + @overrides(component.Component) + def update(self): + if not component.get('ConsoleUI').is_active_mode(self): + return + # Update just the status bars + self.draw_statusbars(bottom_row=-2, bottombar_help=False) + move_cursor(self.stdscr, self.rows - 1, min(self.input_cursor, curses.COLS - 1)) + self.stdscr.refresh() + + @overrides(BaseMode) + def pause(self): + self.stdscr.leaveok(0) + + @overrides(BaseMode) + def resume(self): + util.safe_curs_set(util.Curser.VERY_VISIBLE) + + @overrides(BaseMode) + def read_input(self): + # Read the character + c = self.stdscr.getch() + + # Either ESC or ALT+<some key> + if c == util.KEY_ESC: + n = self.stdscr.getch() + if n == -1: + # Escape key + return + c = [c, n] + + # We remove the tab count if the key wasn't a tab + if c != util.KEY_TAB: + self.tab_count = 0 + + # We clear the input string and send it to the command parser on ENTER + if c in [curses.KEY_ENTER, util.KEY_ENTER2]: + if self.input: + if self.input.endswith('\\'): + self.input = self.input[:-1] + self.input_cursor -= 1 + self.add_line('{!yellow,black,bold!}>>>{!input!} %s' % self.input) + self.do_command(self.input) + if len(self.input_history) == INPUT_HISTORY_SIZE: + # Remove the oldest input history if the max history size + # is reached. + del self.input_history[0] + if self.console_config['cmdline']['ignore_duplicate_lines']: + if len(self.input_history) > 0: + if self.input_history[-1] != self.input: + self.input_history.append(self.input) + else: + self.input_history.append(self.input) + else: + self.input_history.append(self.input) + self.input_history_index = len(self.input_history) + self.input = '' + self.input_incomplete = '' + self.input_cursor = 0 + self.stdscr.refresh() + + # Run the tab completer function + elif c == util.KEY_TAB: + # Keep track of tab hit count to know when it's double-hit + self.tab_count += 1 + + if self.tab_completer: + # We only call the tab completer function if we're at the end of + # the input string on the cursor is on a space + if ( + self.input_cursor == len(self.input) + or self.input[self.input_cursor] == ' ' + ): + self.input, self.input_cursor = self.tab_completer( + self.input, self.input_cursor, self.tab_count + ) + + # We use the UP and DOWN keys to cycle through input history + elif c == curses.KEY_UP: + if self.input_history_index - 1 >= 0: + if self.input_history_index == len(self.input_history): + # We're moving from non-complete input so save it just incase + # we move back down to it. + self.input_incomplete = self.input + # Going back in the history + self.input_history_index -= 1 + self.input = self.input_history[self.input_history_index] + self.input_cursor = len(self.input) + elif c == curses.KEY_DOWN: + if self.input_history_index + 1 < len(self.input_history): + # Going forward in the history + self.input_history_index += 1 + self.input = self.input_history[self.input_history_index] + self.input_cursor = len(self.input) + elif self.input_history_index + 1 == len(self.input_history): + # We're moving back down to an incomplete input + self.input_history_index += 1 + self.input = self.input_incomplete + self.input_cursor = len(self.input) + + # Cursor movement + elif c == curses.KEY_LEFT: + if self.input_cursor: + self.input_cursor -= 1 + elif c == curses.KEY_RIGHT: + if self.input_cursor < len(self.input): + self.input_cursor += 1 + elif c == curses.KEY_HOME: + self.input_cursor = 0 + elif c == curses.KEY_END: + self.input_cursor = len(self.input) + + # Scrolling through buffer + elif c == curses.KEY_PPAGE: + self.display_lines_offset += self.rows - 3 + # We substract 3 for the unavailable lines and 1 extra due to len(self.lines) + if self.display_lines_offset > (len(self.lines) - 4 - self.rows): + self.display_lines_offset = len(self.lines) - 4 - self.rows + + self.refresh() + elif c == curses.KEY_NPAGE: + self.display_lines_offset -= self.rows - 3 + if self.display_lines_offset < 0: + self.display_lines_offset = 0 + self.refresh() + + # Delete a character in the input string based on cursor position + elif c in [curses.KEY_BACKSPACE, util.KEY_BACKSPACE2]: + if self.input and self.input_cursor > 0: + self.input = ( + self.input[: self.input_cursor - 1] + + self.input[self.input_cursor :] + ) + self.input_cursor -= 1 + # Delete a word when alt+backspace is pressed + elif c == [util.KEY_ESC, util.KEY_BACKSPACE2] or c == [ + util.KEY_ESC, + curses.KEY_BACKSPACE, + ]: + self.input, self.input_cursor = delete_alt_backspace( + self.input, self.input_cursor + ) + elif c == curses.KEY_DC: + if self.input and self.input_cursor < len(self.input): + self.input = ( + self.input[: self.input_cursor] + + self.input[self.input_cursor + 1 :] + ) + + # A key to add to the input string + else: + if 31 < c < 256: + # Emulate getwch + stroke = chr(c) + uchar = stroke + while not uchar: + try: + uchar = stroke.decode(self.encoding) + except UnicodeDecodeError: + c = self.stdscr.getch() + stroke += chr(c) + + if uchar: + if self.input_cursor == len(self.input): + self.input += uchar + else: + # Insert into string + self.input = ( + self.input[: self.input_cursor] + + uchar + + self.input[self.input_cursor :] + ) + + # Move the cursor forward + self.input_cursor += 1 + + self.refresh() + + @overrides(BaseMode) + def on_resize(self, rows, cols): + BaseMode.on_resize(self, rows, cols) + self.stdscr.erase() + self.refresh() + + @overrides(BaseMode) + def refresh(self): + """ + Refreshes the screen. + Updates the lines based on the`:attr:lines` based on the `:attr:display_lines_offset` + attribute and the status bars. + """ + if not component.get('ConsoleUI').is_active_mode(self): + return + self.stdscr.erase() + + # Update the status bars + self.add_string(0, self.statusbars.topbar) + self.add_string(self.rows - 2, self.statusbars.bottombar) + + # The number of rows minus the status bars and the input line + available_lines = self.rows - 3 + # If the amount of lines exceeds the number of rows, we need to figure out + # which ones to display based on the offset + if len(self.lines) > available_lines: + # Get the lines to display based on the offset + offset = len(self.lines) - self.display_lines_offset + lines = self.lines[-(available_lines - offset) : offset] + elif len(self.lines) == available_lines: + lines = self.lines + else: + lines = [''] * (available_lines - len(self.lines)) + lines.extend(self.lines) + + # Add the lines to the screen + for index, line in enumerate(lines): + self.add_string(index + 1, line) + + # Add the input string + self.add_string(self.rows - 1, self.input, pad=False, trim=False) + + move_cursor(self.stdscr, self.rows - 1, min(self.input_cursor, curses.COLS - 1)) + self.stdscr.redrawwin() + self.stdscr.refresh() + + def add_line(self, text, refresh=True): + """ + Add a line to the screen. This will be showed between the two bars. + The text can be formatted with color using the following format: + + "{!fg, bg, attributes, ...!}" + + See: http://docs.python.org/library/curses.html#constants for attributes. + + Alternatively, it can use some built-in scheme for coloring. + See colors.py for built-in schemes. + + "{!scheme!}" + + Examples: + + "{!blue, black, bold!}My Text is {!white, black!}cool" + "{!info!}I am some info text!" + "{!error!}Uh oh!" + + :param text: the text to show + :type text: string + :param refresh: if True, the screen will refresh after the line is added + :type refresh: bool + + """ + + if self.console_config['cmdline']['save_command_history']: + # Determine which file is the active one + # If both are under maximum, it's first, otherwise it's the one not full + if ( + self._hf_lines[0] < MAX_HISTFILE_SIZE + and self._hf_lines[1] < MAX_HISTFILE_SIZE + ): + active_file = 0 + elif self._hf_lines[0] == MAX_HISTFILE_SIZE: + active_file = 1 + else: + active_file = 0 + + # Write the line + with open(self.history_file[active_file], 'a', encoding='utf8') as _file: + _file.write(text + '\n') + + # And increment line counter + self._hf_lines[active_file] += 1 + + # If the active file reaches max size, we truncate it + # therefore swapping the currently active file + if self._hf_lines[active_file] == MAX_HISTFILE_SIZE: + self._hf_lines[1 - active_file] = 0 + with open( + self.history_file[1 - active_file], 'w', encoding='utf8' + ) as _file: + _file.truncate(0) + + def get_line_chunks(line): + """ + Returns a list of 2-tuples (color string, text) + + """ + if not line or line.count('{!') != line.count('!}'): + return [] + + chunks = [] + if not line.startswith('{!'): + begin = line.find('{!') + if begin == -1: + begin = len(line) + chunks.append(('', line[:begin])) + line = line[begin:] + + while line: + # We know the line starts with "{!" here + end_color = line.find('!}') + next_color = line.find('{!', end_color) + if next_color == -1: + next_color = len(line) + chunks.append((line[: end_color + 2], line[end_color + 2 : next_color])) + line = line[next_color:] + return chunks + + for line in text.splitlines(): + # We need to check for line lengths here and split as necessary + try: + line_length = colors.get_line_width(line) + except colors.BadColorString: + log.error('Passed a bad colored line: %s', line) + continue + + if line_length >= (self.cols - 1): + s = '' + # The length of the text without the color tags + s_len = 0 + # We need to split this over multiple lines + for chunk in get_line_chunks(line): + if (strwidth(chunk[1]) + s_len) < (self.cols - 1): + # This chunk plus the current string in 's' isn't over + # the maximum width, so just append the color tag and text + s += chunk[0] + chunk[1] + s_len += strwidth(chunk[1]) + else: + # The chunk plus the current string in 's' is too long. + # We need to take as much of the chunk and put it into 's' + # with the color tag. + remain = (self.cols - 1) - s_len + s += chunk[0] + chunk[1][:remain] + # We append the line since it's full + self.lines.append(s) + # Start a new 's' with the remainder chunk + s = chunk[0] + chunk[1][remain:] + s_len = strwidth(chunk[1][remain:]) + # Append the final string which may or may not be the full width + if s: + self.lines.append(s) + else: + self.lines.append(line) + + while len(self.lines) > LINES_BUFFER_SIZE: + # Remove the oldest line if the max buffer size has been reached + del self.lines[0] + + if refresh: + self.refresh() + + def _add_string(self, row, string): + """ + Adds a string to the desired `:param:row`. + + :param row: int, the row number to write the string + + """ + col = 0 + try: + parsed = colors.parse_color_string(string) + except colors.BadColorString as ex: + log.error('Cannot add bad color string %s: %s', string, ex) + return + + for index, (color, p_str) in enumerate(parsed): + if index + 1 == len(parsed): + # This is the last string so lets append some " " to it + p_str += ' ' * (self.cols - (col + strwidth(p_str)) - 1) + try: + self.stdscr.addstr(row, col, p_str.encode(self.encoding), color) + except curses.error: + pass + + col += strwidth(p_str) + + def set_batch_write(self, batch): + """ + When this is set the screen is not refreshed after a `:meth:write` until + this is set to False. + + :param batch: set True to prevent screen refreshes after a `:meth:write` + :type batch: bool + + """ + self.batch_write = batch + if not batch: + self.refresh() + + def write(self, line): + """ + Writes a line out + + :param line: str, the line to print + + """ + + self.add_line(line, not self.batch_write) + + def tab_completer(self, line, cursor, hits): + """ + Called when the user hits 'tab' and will autocomplete or show options. + If a command is already supplied in the line, this function will call the + complete method of the command. + + :param line: str, the current input string + :param cursor: int, the cursor position in the line + :param second_hit: bool, if this is the second time in a row the tab key + has been pressed + + :returns: 2-tuple (string, cursor position) + + """ + # First check to see if there is no space, this will mean that it's a + # command that needs to be completed. + + # We don't want to split by escaped spaces + def split(string): + return re.split(r'(?<!\\) ', string) + + if ' ' not in line: + possible_matches = [] + # Iterate through the commands looking for ones that startwith the + # line. + for cmd in self.console._commands: + if cmd.startswith(line): + possible_matches.append(cmd) + + line_prefix = '' + else: + cmd = split(line)[0] + if cmd in self.console._commands: + # Call the command's complete method to get 'er done + possible_matches = self.console._commands[cmd].complete(split(line)[-1]) + line_prefix = ' '.join(split(line)[:-1]) + ' ' + else: + # This is a bogus command + return (line, cursor) + + # No matches, so just return what we got passed + if len(possible_matches) == 0: + return (line, cursor) + # If we only have 1 possible match, then just modify the line and + # return it, else we need to print out the matches without modifying + # the line. + elif len(possible_matches) == 1: + # Do not append space after directory names + new_line = line_prefix + possible_matches[0] + if not new_line.endswith('/') and not new_line.endswith(r'\\'): + new_line += ' ' + # We only want to print eventual colors or other control characters, not return them + new_line = remove_formatting(new_line) + return (new_line, len(new_line)) + else: + if hits == 1: + p = ' '.join(split(line)[:-1]) + + try: + l_arg = split(line)[-1] + except IndexError: + l_arg = '' + + new_line = ' '.join( + [p, complete_line(l_arg, possible_matches)] + ).lstrip() + + if len(remove_formatting(new_line)) > len(line): + line = new_line + cursor = len(line) + elif hits >= 2: + max_list = self.console_config['cmdline']['torrents_per_tab_press'] + match_count = len(possible_matches) + listed = (hits - 2) * max_list + pages = (match_count - 1) // max_list + 1 + left = match_count - listed + if hits == 2: + self.write(' ') + + if match_count >= 4: + self.write('{!green!}Autocompletion matches:') + # Only list some of the matching torrents as there can be hundreds of them + if self.console_config['cmdline']['third_tab_lists_all']: + if hits == 2 and left > max_list: + for i in range(listed, listed + max_list): + match = possible_matches[i] + self.write(match.replace(r'\ ', ' ')) + self.write( + '{!error!}And %i more. Press <tab> to list them' + % (left - max_list) + ) + else: + self.tab_count = 0 + for match in possible_matches[listed:]: + self.write(match.replace(r'\ ', ' ')) + else: + if left > max_list: + for i in range(listed, listed + max_list): + match = possible_matches[i] + self.write(match.replace(r'\ ', ' ')) + self.write( + '{!error!}And %i more (%i/%i). Press <tab> to view more' + % (left - max_list, hits - 1, pages) + ) + else: + self.tab_count = 0 + for match in possible_matches[listed:]: + self.write(match.replace(r'\ ', ' ')) + if hits > 2: + self.write( + '{!green!}Finished listing %i torrents (%i/%i)' + % (match_count, hits - 1, pages) + ) + + # We only want to print eventual colors or other control characters, not return them + line = remove_formatting(line) + cursor = len(line) + return (line, cursor) + + def tab_complete_path( + self, line, path_type='file', ext='', sort='name', dirs_first=1 + ): + self.console = component.get('ConsoleUI') + + line = line.replace('\\ ', ' ') + line = os.path.abspath(os.path.expanduser(line)) + ret = [] + if os.path.exists(line): + # This is a correct path, check to see if it's a directory + if os.path.isdir(line): + # Directory, so we need to show contents of directory + # ret.extend(os.listdir(line)) + try: + for f in os.listdir(line): + # Skip hidden + if f.startswith('.'): + continue + f = os.path.join(line, f) + if os.path.isdir(f): + if os.sep == '\\': # Windows path support + f += '\\' + else: # Unix + f += '/' + elif not f.endswith(ext): + continue + ret.append(f) + except OSError: + self.console.write('{!error!}Permission denied: {!info!}%s' % line) + else: + try: + # This is a file, but we could be looking for another file that + # shares a common prefix. + for f in os.listdir(os.path.dirname(line)): + if f.startswith(os.path.split(line)[1]): + ret.append(os.path.join(os.path.dirname(line), f)) + except OSError: + self.console.write('{!error!}Permission denied: {!info!}%s' % line) + else: + # This path does not exist, so lets do a listdir on it's parent + # and find any matches. + try: + ret = [] + if os.path.isdir(os.path.dirname(line)): + for f in os.listdir(os.path.dirname(line)): + if f.startswith(os.path.split(line)[1]): + p = os.path.join(os.path.dirname(line), f) + + if os.path.isdir(p): + if os.sep == '\\': # Windows path support + p += '\\' + else: # Unix + p += '/' + ret.append(p) + except OSError: + self.console.write('{!error!}Permission denied: {!info!}%s' % line) + + if sort == 'date': + ret = sorted(ret, key=os.path.getmtime, reverse=True) + + if dirs_first == 1: + ret = sorted(ret, key=os.path.isdir, reverse=True) + elif dirs_first == -1: + ret = sorted(ret, key=os.path.isdir, reverse=False) + + # Highlight directory names + for i, filename in enumerate(ret): + if os.path.isdir(filename): + ret[i] = '{!cyan!}%s' % filename + + for i in range(0, len(ret)): + ret[i] = ret[i].replace(' ', r'\ ') + return ret + + def tab_complete_torrent(self, line): + """ + Completes torrent_ids or names. + + :param line: str, the string to complete + + :returns: list of matches + + """ + + empty = len(line) == 0 + + # Remove dangling backslashes to avoid breaking shlex + if line.endswith('\\'): + line = line[:-1] + + raw_line = line + line = line.replace('\\ ', ' ') + + possible_matches = [] + possible_matches2 = [] + + match_count = 0 + match_count2 = 0 + for torrent_id, torrent_name in self.console.torrents: + if torrent_id.startswith(line): + match_count += 1 + if torrent_name.startswith(line): + match_count += 1 + elif torrent_name.lower().startswith(line.lower()): + match_count2 += 1 + + # Find all possible matches + for torrent_id, torrent_name in self.console.torrents: + # Escape spaces to avoid, for example, expanding "Doc" into "Doctor Who" and removing + # everything containing one of these words + escaped_name = torrent_name.replace(' ', '\\ ') + # If we only matched one torrent, don't add the full name or it'll also get autocompleted + if match_count == 1: + if torrent_id.startswith(line): + possible_matches.append(torrent_id) + break + if torrent_name.startswith(line): + possible_matches.append(escaped_name) + break + elif match_count == 0 and match_count2 == 1: + if torrent_name.lower().startswith(line.lower()): + possible_matches.append(escaped_name) + break + else: + line_len = len(raw_line) + + # Let's avoid listing all torrents twice if there's no pattern + if not empty and torrent_id.startswith(line): + # Highlight the matching part + text = '{{!info!}}{}{{!input!}}{} - "{}"'.format( + torrent_id[:line_len], + torrent_id[line_len:], + torrent_name, + ) + possible_matches.append(text) + if torrent_name.startswith(line): + text = '{{!info!}}{}{{!input!}}{} ({{!cyan!}}{}{{!input!}})'.format( + escaped_name[:line_len], + escaped_name[line_len:], + torrent_id, + ) + possible_matches.append(text) + elif torrent_name.lower().startswith(line.lower()): + text = '{{!info!}}{}{{!input!}}{} ({{!cyan!}}{}{{!input!}})'.format( + escaped_name[:line_len], + escaped_name[line_len:], + torrent_id, + ) + possible_matches2.append(text) + + return possible_matches + possible_matches2 |