From 2e2851dc13d73352530dd4495c7e05603b2e520d Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 10 Apr 2024 23:38:38 +0200 Subject: Adding upstream version 2.1.2~dev0+20240219. Signed-off-by: Daniel Baumann --- deluge/ui/console/modes/torrentlist/torrentview.py | 514 +++++++++++++++++++++ 1 file changed, 514 insertions(+) create mode 100644 deluge/ui/console/modes/torrentlist/torrentview.py (limited to 'deluge/ui/console/modes/torrentlist/torrentview.py') diff --git a/deluge/ui/console/modes/torrentlist/torrentview.py b/deluge/ui/console/modes/torrentlist/torrentview.py new file mode 100644 index 0000000..1ce5097 --- /dev/null +++ b/deluge/ui/console/modes/torrentlist/torrentview.py @@ -0,0 +1,514 @@ +# +# 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 deluge.component as component +from deluge.decorators import overrides +from deluge.ui.console.modes.basemode import InputKeyHandler +from deluge.ui.console.modes.torrentlist import torrentviewcolumns +from deluge.ui.console.modes.torrentlist.torrentactions import torrent_actions_popup +from deluge.ui.console.utils import curses_util as util +from deluge.ui.console.utils import format_utils +from deluge.ui.console.utils.column import ( + get_column_value, + get_required_fields, + torrent_data_fields, +) + +from . import ACTION + +try: + import curses +except ImportError: + pass + +log = logging.getLogger(__name__) + + +state_fg_colors = { + 'Downloading': 'green', + 'Seeding': 'cyan', + 'Error': 'red', + 'Queued': 'yellow', + 'Checking': 'blue', + 'Moving': 'green', +} + + +reverse_sort_fields = [ + 'size', + 'download_speed', + 'upload_speed', + 'num_seeds', + 'num_peers', + 'distributed_copies', + 'time_added', + 'total_uploaded', + 'all_time_download', + 'total_remaining', + 'progress', + 'ratio', + 'seeding_time', + 'active_time', +] + + +default_column_values = { + 'queue': {'width': 4, 'visible': True}, + 'name': {'width': -1, 'visible': True}, + 'size': {'width': 8, 'visible': True}, + 'progress': {'width': 7, 'visible': True}, + 'download_speed': {'width': 7, 'visible': True}, + 'upload_speed': {'width': 7, 'visible': True}, + 'state': {'width': 13}, + 'eta': {'width': 8, 'visible': True}, + 'time_added': {'width': 15}, + 'tracker': {'width': 15}, + 'download_location': {'width': 15}, + 'downloaded': {'width': 13}, + 'uploaded': {'width': 7}, + 'remaining': {'width': 13}, + 'completed_time': {'width': 15}, + 'last_seen_complete': {'width': 15}, + 'max_upload_speed': {'width': 7}, +} + + +default_columns = {} +for col_i, col_name in enumerate(torrentviewcolumns.column_pref_names): + default_columns[col_name] = {'width': 10, 'order': col_i, 'visible': False} + if col_name in default_column_values: + default_columns[col_name].update(default_column_values[col_name]) + + +class TorrentView(InputKeyHandler): + def __init__(self, torrentlist, config): + super().__init__() + self.torrentlist = torrentlist + self.config = config + self.filter_dict = {} + self.curr_filter = None + self.cached_rows = {} + self.sorted_ids = None + self.torrent_names = None + self.numtorrents = -1 + self.column_string = '' + self.curoff = 0 + self.marked = [] + self.cursel = 0 + + @property + def rows(self): + return self.torrentlist.rows + + @property + def torrent_rows(self): + return self.torrentlist.rows - 3 # Account for header lines + columns line + + @property + def torrentlist_offset(self): + return 2 + + def update_state(self, state, refresh=False): + self.curstate = state # cache in case we change sort order + self.cached_rows.clear() + self.numtorrents = len(state) + self.sorted_ids = self._sort_torrents(state) + self.torrent_names = [] + for torrent_id in self.sorted_ids: + ts = self.curstate[torrent_id] + self.torrent_names.append(ts['name']) + + if refresh: + self.torrentlist.refresh() + + def set_torrent_filter(self, state): + self.curr_filter = state + filter_dict = {'state': [state]} + if state == 'All': + self.curr_filter = None + filter_dict = {} + self.filter_dict = filter_dict + self.torrentlist.go_top = True + self.torrentlist.update() + return True + + def _scroll_up(self, by): + cursel = self.cursel + prevoff = self.curoff + self.cursel = max(self.cursel - by, 0) + if self.cursel < self.curoff: + self.curoff = self.cursel + affected = [] + if prevoff == self.curoff: + affected.append(cursel) + if cursel != self.cursel: + affected.insert(0, self.cursel) + return affected + + def _scroll_down(self, by): + cursel = self.cursel + prevoff = self.curoff + self.cursel = min(self.cursel + by, self.numtorrents - 1) + if (self.curoff + self.torrent_rows) <= self.cursel: + self.curoff = self.cursel - self.torrent_rows + 1 + affected = [] + if prevoff == self.curoff: + affected.append(cursel) + if cursel != self.cursel: + affected.append(self.cursel) + return affected + + def current_torrent_id(self): + if not self.sorted_ids: + return None + return self.sorted_ids[self.cursel] + + def _selected_torrent_ids(self): + if not self.sorted_ids: + return None + ret = [] + for i in self.marked: + ret.append(self.sorted_ids[i]) + return ret + + def clear_marked(self): + self.marked = [] + self.last_mark = -1 + + def mark_unmark(self, idx): + if idx in self.marked: + self.marked.remove(idx) + self.last_mark = -1 + else: + self.marked.append(idx) + self.last_mark = idx + + def add_marked(self, indices, last_marked): + for i in indices: + if i not in self.marked: + self.marked.append(i) + self.last_mark = last_marked + + def update_marked(self, index, last_mark=True, clear=False): + if index not in self.marked: + if clear: + self.marked = [] + self.marked.append(index) + if last_mark: + self.last_mark = index + return True + return False + + def _sort_torrents(self, state): + """Sorts by primary and secondary sort fields.""" + + if not state: + return {} + + s_primary = self.config['torrentview']['sort_primary'] + s_secondary = self.config['torrentview']['sort_secondary'] + + result = state + + # Sort first by secondary sort field and then primary sort field + # so it all works out + + def sort_by_field(state, to_sort, field): + field = torrent_data_fields[field]['status'][0] + reverse = field in reverse_sort_fields + + # Get first element so we can check if it has given field + # and if it's a string + first_element = state[list(state)[0]] + if field in first_element: + + def sort_key(s): + try: + # Sort case-insensitively but preserve A>a order. + return state.get(s)[field].lower() + except AttributeError: + # Not a string. + return state.get(s)[field] + + to_sort = sorted(to_sort, key=sort_key, reverse=reverse) + + if field == 'eta': + to_sort = sorted(to_sort, key=lambda s: state.get(s)['eta'] == 0) + + return to_sort + + # Just in case primary and secondary fields are empty and/or + # both are too ambiguous, also sort by queue position first + if 'queue' not in [s_secondary, s_primary]: + result = sort_by_field(state, result, 'queue') + if s_secondary != s_primary: + result = sort_by_field(state, result, s_secondary) + result = sort_by_field(state, result, s_primary) + + if self.config['torrentview']['separate_complete']: + result = sorted( + result, key=lambda s: state.get(s).get('progress', 0) == 100.0 + ) + + return result + + def _get_colors(self, row, tidx): + # default style + colors = {'fg': 'white', 'bg': 'black', 'attr': None} + + if tidx in self.marked: + colors.update({'bg': 'blue', 'attr': 'bold'}) + + if tidx == self.cursel: + col_selected = {'bg': 'white', 'fg': 'black', 'attr': 'bold'} + if tidx in self.marked: + col_selected['fg'] = 'blue' + colors.update(col_selected) + + colors['fg'] = state_fg_colors.get(row[1], colors['fg']) + + if self.torrentlist.minor_mode: + self.torrentlist.minor_mode.update_colors(tidx, colors) + return colors + + def update_torrents(self, lines): + # add all the torrents + if self.numtorrents == 0: + cols = self.torrentlist.torrentview_columns() + msg = 'No torrents match filter'.center(cols) + self.torrentlist.add_string( + 3, '{!info!}%s' % msg, scr=self.torrentlist.torrentview_panel + ) + elif self.numtorrents == 0: + self.torrentlist.add_string(1, 'Waiting for torrents from core...') + return + + def draw_row(index): + if index not in self.cached_rows: + ts = self.curstate[self.sorted_ids[index]] + self.cached_rows[index] = ( + format_utils.format_row( + [get_column_value(name, ts) for name in self.cols_to_show], + self.column_widths, + ), + ts['state'], + ) + return self.cached_rows[index] + + tidx = self.curoff + currow = 0 + todraw = [] + # Affected lines are given when changing selected torrent + if lines: + for line in lines: + if line < tidx: + continue + if line >= (tidx + self.torrent_rows) or line >= self.numtorrents: + break + todraw.append((line, line - self.curoff, draw_row(line))) + else: + for i in range(tidx, tidx + self.torrent_rows): + if i >= self.numtorrents: + break + todraw.append((i, i - self.curoff, draw_row(i))) + + for tidx, currow, row in todraw: + if (currow + self.torrentlist_offset - 1) > self.torrent_rows: + continue + colors = self._get_colors(row, tidx) + if colors['attr']: + colorstr = '{!%(fg)s,%(bg)s,%(attr)s!}' % colors + else: + colorstr = '{!%(fg)s,%(bg)s!}' % colors + + self.torrentlist.add_string( + currow + self.torrentlist_offset, + f'{colorstr}{row[0]}', + trim=False, + scr=self.torrentlist.torrentview_panel, + ) + + def update(self, refresh=False): + d = component.get('SessionProxy').get_torrents_status( + self.filter_dict, self.status_fields + ) + d.addCallback(self.update_state, refresh=refresh) + + def on_config_changed(self): + s_primary = self.config['torrentview']['sort_primary'] + s_secondary = self.config['torrentview']['sort_secondary'] + changed = None + for col in default_columns: + if col not in self.config['torrentview']['columns']: + changed = self.config['torrentview']['columns'][col] = default_columns[ + col + ] + if changed: + self.config.save() + + self.cols_to_show = [ + col + for col in sorted( + self.config['torrentview']['columns'], + key=lambda k: self.config['torrentview']['columns'][k]['order'], + ) + if self.config['torrentview']['columns'][col]['visible'] + ] + self.status_fields = get_required_fields(self.cols_to_show) + + # we always need these, even if we're not displaying them + for rf in ['state', 'name', 'queue', 'progress']: + if rf not in self.status_fields: + self.status_fields.append(rf) + + # same with sort keys + if s_primary and s_primary not in self.status_fields: + self.status_fields.append(s_primary) + if s_secondary and s_secondary not in self.status_fields: + self.status_fields.append(s_secondary) + + self.update_columns() + + def update_columns(self): + self.column_widths = [ + self.config['torrentview']['columns'][col]['width'] + for col in self.cols_to_show + ] + requested_width = sum(width for width in self.column_widths if width >= 0) + + cols = self.torrentlist.torrentview_columns() + if requested_width > cols: # can't satisfy requests, just spread out evenly + cw = int(cols / len(self.cols_to_show)) + for i in range(0, len(self.column_widths)): + self.column_widths[i] = cw + else: + rem = cols - requested_width + var_cols = len([width for width in self.column_widths if width < 0]) + if var_cols > 0: + vw = int(rem / var_cols) + for i in range(0, len(self.column_widths)): + if self.column_widths[i] < 0: + self.column_widths[i] = vw + + self.column_string = '{!header!}' + + primary_sort_col_name = self.config['torrentview']['sort_primary'] + + for i, column in enumerate(self.cols_to_show): + ccol = torrent_data_fields[column]['name'] + width = self.column_widths[i] + + # Trim the column if it's too long to fit + if len(ccol) > width: + ccol = ccol[: width - 1] + + # Padding + ccol += ' ' * (width - len(ccol)) + + # Highlight the primary sort column + if column == primary_sort_col_name: + if i != len(self.cols_to_show) - 1: + ccol = '{!black,green,bold!}%s{!header!}' % ccol + else: + ccol = ('{!black,green,bold!}%s' % ccol)[:-1] + + self.column_string += ccol + + @overrides(InputKeyHandler) + def handle_read(self, c): + affected_lines = None + if c == curses.KEY_UP: + if self.cursel != 0: + affected_lines = self._scroll_up(1) + elif c == curses.KEY_PPAGE: + affected_lines = self._scroll_up(int(self.torrent_rows / 2)) + elif c == curses.KEY_DOWN: + if self.cursel < self.numtorrents: + affected_lines = self._scroll_down(1) + elif c == curses.KEY_NPAGE: + affected_lines = self._scroll_down(int(self.torrent_rows / 2)) + elif c == curses.KEY_HOME: + affected_lines = self._scroll_up(self.cursel) + elif c == curses.KEY_END: + affected_lines = self._scroll_down(self.numtorrents - self.cursel) + elif c == curses.KEY_DC: # DEL + added = self.update_marked(self.cursel) + + def on_close(**kwargs): + if added: + self.marked.pop() + + torrent_actions_popup( + self.torrentlist, + self._selected_torrent_ids(), + action=ACTION.REMOVE, + close_cb=on_close, + ) + elif c in [curses.KEY_ENTER, util.KEY_ENTER2] and self.numtorrents: + added = self.update_marked(self.cursel) + + def on_close(data, **kwargs): + if added: + self.marked.remove(self.cursel) + + torrent_actions_popup( + self.torrentlist, + self._selected_torrent_ids(), + details=True, + close_cb=on_close, + ) + self.torrentlist.refresh() + elif c == ord('j'): + affected_lines = self._scroll_down(1) + elif c == ord('k'): + affected_lines = self._scroll_up(1) + elif c == ord('m'): + self.mark_unmark(self.cursel) + affected_lines = [self.cursel] + elif c == ord('M'): + if self.last_mark >= 0: + if self.cursel > self.last_mark: + mrange = list(range(self.last_mark, self.cursel + 1)) + else: + mrange = list(range(self.cursel, self.last_mark)) + self.add_marked(mrange, self.cursel) + affected_lines = mrange + else: + self.mark_unmark(self.cursel) + affected_lines = [self.cursel] + elif c == ord('c'): + self.clear_marked() + elif c == ord('o'): + if not self.marked: + added = self.update_marked(self.cursel, clear=True) + else: + self.last_mark = -1 + torrent_actions_popup( + self.torrentlist, + self._selected_torrent_ids(), + action=ACTION.TORRENT_OPTIONS, + ) + elif c in [ord('>'), ord('<')]: + try: + i = self.cols_to_show.index(self.config['torrentview']['sort_primary']) + except ValueError: + i = 0 if chr(c) == '<' else len(self.cols_to_show) + else: + i += 1 if chr(c) == '>' else -1 + + i = max(0, min(len(self.cols_to_show) - 1, i)) + self.config['torrentview']['sort_primary'] = self.cols_to_show[i] + self.config.save() + self.on_config_changed() + self.update_columns() + self.torrentlist.refresh([]) + else: + return util.ReadState.IGNORED + + self.set_input_result(affected_lines) + return util.ReadState.CHANGED if affected_lines else util.ReadState.READ -- cgit v1.2.3