summaryrefslogtreecommitdiffstats
path: root/deluge/ui/console/modes/torrentlist
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2023-02-19 14:52:21 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2023-02-19 14:52:21 +0000
commitd1772d410235592b482e3b08b1863f6624d9fe6b (patch)
treeaccfb4b99c32e5e435089f8023d62e67a6951603 /deluge/ui/console/modes/torrentlist
parentInitial commit. (diff)
downloaddeluge-d1772d410235592b482e3b08b1863f6624d9fe6b.tar.xz
deluge-d1772d410235592b482e3b08b1863f6624d9fe6b.zip
Adding upstream version 2.0.3.upstream/2.0.3
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'deluge/ui/console/modes/torrentlist')
-rw-r--r--deluge/ui/console/modes/torrentlist/__init__.py20
-rw-r--r--deluge/ui/console/modes/torrentlist/add_torrents_popup.py113
-rw-r--r--deluge/ui/console/modes/torrentlist/filtersidebar.py134
-rw-r--r--deluge/ui/console/modes/torrentlist/queue_mode.py157
-rw-r--r--deluge/ui/console/modes/torrentlist/search_mode.py210
-rw-r--r--deluge/ui/console/modes/torrentlist/torrentactions.py276
-rw-r--r--deluge/ui/console/modes/torrentlist/torrentlist.py348
-rw-r--r--deluge/ui/console/modes/torrentlist/torrentview.py517
-rw-r--r--deluge/ui/console/modes/torrentlist/torrentviewcolumns.py162
9 files changed, 1937 insertions, 0 deletions
diff --git a/deluge/ui/console/modes/torrentlist/__init__.py b/deluge/ui/console/modes/torrentlist/__init__.py
new file mode 100644
index 0000000..18c4db3
--- /dev/null
+++ b/deluge/ui/console/modes/torrentlist/__init__.py
@@ -0,0 +1,20 @@
+from __future__ import unicode_literals
+
+
+class ACTION(object):
+ PAUSE = 'pause'
+ RESUME = 'resume'
+ REANNOUNCE = 'update_tracker'
+ EDIT_TRACKERS = 3
+ RECHECK = 'force_recheck'
+ REMOVE = 'remove_torrent'
+ REMOVE_DATA = 6
+ REMOVE_NODATA = 7
+ DETAILS = 'torrent_details'
+ MOVE_STORAGE = 'move_download_folder'
+ QUEUE = 'queue'
+ QUEUE_TOP = 'queue_top'
+ QUEUE_UP = 'queue_up'
+ QUEUE_DOWN = 'queue_down'
+ QUEUE_BOTTOM = 'queue_bottom'
+ TORRENT_OPTIONS = 'torrent_options'
diff --git a/deluge/ui/console/modes/torrentlist/add_torrents_popup.py b/deluge/ui/console/modes/torrentlist/add_torrents_popup.py
new file mode 100644
index 0000000..b0ac483
--- /dev/null
+++ b/deluge/ui/console/modes/torrentlist/add_torrents_popup.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
+#
+# 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.
+#
+
+from __future__ import unicode_literals
+
+import logging
+
+import deluge.common
+from deluge.ui.client import client
+from deluge.ui.console.widgets.popup import InputPopup, SelectablePopup
+
+log = logging.getLogger(__name__)
+
+
+def report_add_status(torrentlist, succ_cnt, fail_cnt, fail_msgs):
+ if fail_cnt == 0:
+ torrentlist.report_message(
+ 'Torrents Added', '{!success!}Successfully added %d torrent(s)' % succ_cnt
+ )
+ else:
+ msg = (
+ '{!error!}Failed to add the following %d torrent(s):\n {!input!}' % fail_cnt
+ ) + '\n '.join(fail_msgs)
+ if succ_cnt != 0:
+ msg += '\n \n{!success!}Successfully added %d torrent(s)' % succ_cnt
+ torrentlist.report_message('Torrent Add Report', msg)
+
+
+def show_torrent_add_popup(torrentlist):
+ def do_add_from_url(data=None, **kwargs):
+ torrentlist.pop_popup()
+ if not data or kwargs.get('close', False):
+ return
+
+ def fail_cb(msg, url):
+ log.debug('failed to add torrent: %s: %s', url, msg)
+ error_msg = '{!input!} * %s: {!error!}%s' % (url, msg)
+ report_add_status(torrentlist, 0, 1, [error_msg])
+
+ def success_cb(tid, url):
+ if tid:
+ log.debug('added torrent: %s (%s)', url, tid)
+ report_add_status(torrentlist, 1, 0, [])
+ else:
+ fail_cb('Already in session (probably)', url)
+
+ url = data['url']['value']
+ if not url:
+ return
+
+ t_options = {
+ 'download_location': data['path']['value'],
+ 'add_paused': data['add_paused']['value'],
+ }
+
+ if deluge.common.is_magnet(url):
+ client.core.add_torrent_magnet(url, t_options).addCallback(
+ success_cb, url
+ ).addErrback(fail_cb, url)
+ elif deluge.common.is_url(url):
+ client.core.add_torrent_url(url, t_options).addCallback(
+ success_cb, url
+ ).addErrback(fail_cb, url)
+ else:
+ torrentlist.report_message(
+ 'Error', '{!error!}Invalid URL or magnet link: %s' % url
+ )
+ return
+
+ log.debug(
+ 'Adding Torrent(s): %s (dl path: %s) (paused: %d)',
+ url,
+ data['path']['value'],
+ data['add_paused']['value'],
+ )
+
+ def show_add_url_popup():
+ add_paused = 1 if 'add_paused' in torrentlist.coreconfig else 0
+ popup = InputPopup(
+ torrentlist, 'Add Torrent (Esc to cancel)', close_cb=do_add_from_url
+ )
+ popup.add_text_input('url', 'Enter torrent URL or Magnet link:')
+ popup.add_text_input(
+ 'path',
+ 'Enter save path:',
+ torrentlist.coreconfig.get('download_location', ''),
+ complete=True,
+ )
+ popup.add_select_input(
+ 'add_paused', 'Add Paused:', ['Yes', 'No'], [True, False], add_paused
+ )
+ torrentlist.push_popup(popup)
+
+ def option_chosen(selected, *args, **kwargs):
+ if not selected or selected == 'cancel':
+ torrentlist.pop_popup()
+ return
+ if selected == 'file':
+ torrentlist.consoleui.set_mode('AddTorrents')
+ elif selected == 'url':
+ show_add_url_popup()
+
+ popup = SelectablePopup(torrentlist, 'Add torrent', option_chosen)
+ popup.add_line('file', '- From _File(s)', use_underline=True)
+ popup.add_line('url', '- From _URL or Magnet', use_underline=True)
+ popup.add_line('cancel', '- _Cancel', use_underline=True)
+ torrentlist.push_popup(popup, clear=True)
diff --git a/deluge/ui/console/modes/torrentlist/filtersidebar.py b/deluge/ui/console/modes/torrentlist/filtersidebar.py
new file mode 100644
index 0000000..0f39b5c
--- /dev/null
+++ b/deluge/ui/console/modes/torrentlist/filtersidebar.py
@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 bendikro <bro.devel+deluge@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.
+#
+
+from __future__ import unicode_literals
+
+import curses
+import logging
+
+from deluge.component import Component
+from deluge.decorators import overrides
+from deluge.ui.client import client
+from deluge.ui.console.utils import curses_util as util
+from deluge.ui.console.widgets import BaseInputPane
+from deluge.ui.console.widgets.sidebar import Sidebar
+
+log = logging.getLogger(__name__)
+
+
+class FilterSidebar(Sidebar, Component):
+ """The sidebar in the main torrentview
+
+ Shows the different states of the torrents and allows to filter the
+ torrents based on state.
+
+ """
+
+ def __init__(self, torrentlist, config):
+ self.config = config
+ height = curses.LINES - 2
+ width = self.config['torrentview']['sidebar_width']
+ Sidebar.__init__(
+ self,
+ torrentlist,
+ width,
+ height,
+ title=' Filter ',
+ border_off_north=1,
+ allow_resize=True,
+ )
+ Component.__init__(self, 'FilterSidebar')
+ self.checked_index = 0
+ kwargs = {
+ 'checked_char': '*',
+ 'unchecked_char': '-',
+ 'checkbox_format': ' %s ',
+ 'col': 0,
+ }
+ self.add_checked_input('All', 'All', checked=True, **kwargs)
+ self.add_checked_input('Active', 'Active', **kwargs)
+ self.add_checked_input(
+ 'Downloading', 'Downloading', color='green,black', **kwargs
+ )
+ self.add_checked_input('Seeding', 'Seeding', color='cyan,black', **kwargs)
+ self.add_checked_input('Paused', 'Paused', **kwargs)
+ self.add_checked_input('Error', 'Error', color='red,black', **kwargs)
+ self.add_checked_input('Checking', 'Checking', color='blue,black', **kwargs)
+ self.add_checked_input('Queued', 'Queued', **kwargs)
+ self.add_checked_input(
+ 'Allocating', 'Allocating', color='yellow,black', **kwargs
+ )
+ self.add_checked_input('Moving', 'Moving', color='green,black', **kwargs)
+
+ @overrides(Component)
+ def update(self):
+ if not self.hidden() and client.connected():
+ d = client.core.get_filter_tree(True, []).addCallback(
+ self._cb_update_filter_tree
+ )
+
+ def on_filter_tree_updated(changed):
+ if changed:
+ self.refresh()
+
+ d.addCallback(on_filter_tree_updated)
+
+ def _cb_update_filter_tree(self, filter_items):
+ """Callback function on client.core.get_filter_tree"""
+ states = filter_items['state']
+ largest_count = 0
+ largest_state_width = 0
+ for state in states:
+ largest_state_width = max(len(state[0]), largest_state_width)
+ largest_count = max(int(state[1]), largest_count)
+
+ border_and_spacing = 6 # Account for border + whitespace
+ filter_state_width = largest_state_width
+ filter_count_width = self.width - filter_state_width - border_and_spacing
+
+ changed = False
+ for state in states:
+ field = self.get_input(state[0])
+ if field:
+ txt = (
+ '%%-%ds%%%ds'
+ % (filter_state_width, filter_count_width)
+ % (state[0], state[1])
+ )
+ if field.set_message(txt):
+ changed = True
+ return changed
+
+ @overrides(BaseInputPane)
+ def immediate_action_cb(self, state_changed=True):
+ if state_changed:
+ self.parent.torrentview.set_torrent_filter(
+ self.inputs[self.active_input].name
+ )
+
+ @overrides(Sidebar)
+ def handle_read(self, c):
+ if c == util.KEY_SPACE:
+ if self.checked_index != self.active_input:
+ self.inputs[self.checked_index].set_value(False)
+ Sidebar.handle_read(self, c)
+ self.checked_index = self.active_input
+ return util.ReadState.READ
+ else:
+ return Sidebar.handle_read(self, c)
+
+ @overrides(Sidebar)
+ def on_resize(self, width):
+ sidebar_width = self.config['torrentview']['sidebar_width']
+ if sidebar_width != width:
+ self.config['torrentview']['sidebar_width'] = width
+ self.config.save()
+ self.resize_window(self.height, width)
+ self.parent.toggle_sidebar()
+ self.refresh()
diff --git a/deluge/ui/console/modes/torrentlist/queue_mode.py b/deluge/ui/console/modes/torrentlist/queue_mode.py
new file mode 100644
index 0000000..0c44aaf
--- /dev/null
+++ b/deluge/ui/console/modes/torrentlist/queue_mode.py
@@ -0,0 +1,157 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
+#
+# 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.
+#
+
+from __future__ import unicode_literals
+
+from deluge.ui.client import client
+from deluge.ui.console.utils import curses_util as util
+from deluge.ui.console.widgets.popup import MessagePopup, SelectablePopup
+
+from . import ACTION
+
+try:
+ import curses
+except ImportError:
+ pass
+
+key_to_action = {
+ curses.KEY_HOME: ACTION.QUEUE_TOP,
+ curses.KEY_UP: ACTION.QUEUE_UP,
+ curses.KEY_DOWN: ACTION.QUEUE_DOWN,
+ curses.KEY_END: ACTION.QUEUE_BOTTOM,
+}
+QUEUE_MODE_HELP_STR = """
+Change queue position of selected torrents
+
+{!info!}'+'{!normal!} - {|indent_pos:|}Move up
+{!info!}'-'{!normal!} - {|indent_pos:|}Move down
+
+{!info!}'Home'{!normal!} - {|indent_pos:|}Move to top
+{!info!}'End'{!normal!} - {|indent_pos:|}Move to bottom
+
+"""
+
+
+class QueueMode(object):
+ def __init__(self, torrentslist, torrent_ids):
+ self.torrentslist = torrentslist
+ self.torrentview = torrentslist.torrentview
+ self.torrent_ids = torrent_ids
+
+ def set_statusbar_args(self, statusbar_args):
+ statusbar_args[
+ 'bottombar'
+ ] = '{!black,white!}Queue mode: change queue position of selected torrents.'
+ statusbar_args['bottombar_help'] = ' Press [h] for help'
+
+ def update_cursor(self):
+ pass
+
+ def update_colors(self, tidx, colors):
+ pass
+
+ def handle_read(self, c):
+ if c in [util.KEY_ESC, util.KEY_BELL]: # If Escape key or CTRL-g, we abort
+ self.torrentslist.set_minor_mode(None)
+ elif c == ord('h'):
+ popup = MessagePopup(
+ self.torrentslist,
+ 'Help',
+ QUEUE_MODE_HELP_STR,
+ width_req=0.65,
+ border_off_west=1,
+ )
+ self.torrentslist.push_popup(popup, clear=True)
+ elif c in [
+ curses.KEY_UP,
+ curses.KEY_DOWN,
+ curses.KEY_HOME,
+ curses.KEY_END,
+ curses.KEY_NPAGE,
+ curses.KEY_PPAGE,
+ ]:
+ action = key_to_action[c]
+ self.do_queue(action)
+
+ def move_selection(self, cb_arg, qact):
+ if self.torrentslist.config['torrentview']['move_selection'] is False:
+ return
+ queue_length = 0
+ selected_num = 0
+ for tid in self.torrentview.curstate:
+ tq = self.torrentview.curstate[tid]['queue']
+ if tq != -1:
+ queue_length += 1
+ if tq in self.torrentview.marked:
+ selected_num += 1
+ if qact == ACTION.QUEUE_TOP:
+ if self.torrentview.marked:
+ self.torrentview.cursel = 1 + sorted(self.torrentview.marked).index(
+ self.torrentview.cursel
+ )
+ else:
+ self.torrentview.cursel = 1
+ self.torrentview.marked = list(range(1, selected_num + 1))
+ elif qact == ACTION.QUEUE_UP:
+ self.torrentview.cursel = max(1, self.torrentview.cursel - 1)
+ self.torrentview.marked = [marked - 1 for marked in self.torrentview.marked]
+ self.torrentview.marked = [
+ marked for marked in self.torrentview.marked if marked > 0
+ ]
+ elif qact == ACTION.QUEUE_DOWN:
+ self.torrentview.cursel = min(queue_length, self.torrentview.cursel + 1)
+ self.torrentview.marked = [marked + 1 for marked in self.torrentview.marked]
+ self.torrentview.marked = [
+ marked for marked in self.torrentview.marked if marked <= queue_length
+ ]
+ elif qact == ACTION.QUEUE_BOTTOM:
+ if self.torrentview.marked:
+ self.torrentview.cursel = (
+ queue_length
+ - selected_num
+ + 1
+ + sorted(self.torrentview.marked).index(self.torrentview.cursel)
+ )
+ else:
+ self.torrentview.cursel = queue_length
+ self.torrentview.marked = list(
+ range(queue_length - selected_num + 1, queue_length + 1)
+ )
+
+ def do_queue(self, qact, *args, **kwargs):
+ if qact == ACTION.QUEUE_TOP:
+ client.core.queue_top(self.torrent_ids).addCallback(
+ self.move_selection, qact
+ )
+ elif qact == ACTION.QUEUE_BOTTOM:
+ client.core.queue_bottom(self.torrent_ids).addCallback(
+ self.move_selection, qact
+ )
+ elif qact == ACTION.QUEUE_UP:
+ client.core.queue_up(self.torrent_ids).addCallback(
+ self.move_selection, qact
+ )
+ elif qact == ACTION.QUEUE_DOWN:
+ client.core.queue_down(self.torrent_ids).addCallback(
+ self.move_selection, qact
+ )
+
+ def popup(self, **kwargs):
+ popup = SelectablePopup(
+ self.torrentslist,
+ 'Queue Action',
+ self.do_queue,
+ cb_args=kwargs,
+ border_off_west=1,
+ )
+ popup.add_line(ACTION.QUEUE_TOP, '_Top')
+ popup.add_line(ACTION.QUEUE_UP, '_Up')
+ popup.add_line(ACTION.QUEUE_DOWN, '_Down')
+ popup.add_line(ACTION.QUEUE_BOTTOM, '_Bottom')
+ self.torrentslist.push_popup(popup)
diff --git a/deluge/ui/console/modes/torrentlist/search_mode.py b/deluge/ui/console/modes/torrentlist/search_mode.py
new file mode 100644
index 0000000..57a8e5f
--- /dev/null
+++ b/deluge/ui/console/modes/torrentlist/search_mode.py
@@ -0,0 +1,210 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
+#
+# 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.
+#
+
+from __future__ import unicode_literals
+
+import logging
+
+from deluge.common import PY2
+from deluge.decorators import overrides
+from deluge.ui.console.modes.basemode import InputKeyHandler, move_cursor
+from deluge.ui.console.modes.torrentlist.torrentactions import torrent_actions_popup
+from deluge.ui.console.utils import curses_util as util
+
+try:
+ import curses
+except ImportError:
+ pass
+
+log = logging.getLogger(__name__)
+QUEUE_MODE_HELP_STR = """
+Change queue position of selected torrents
+
+{!info!}'+'{!normal!} - {|indent_pos:|}Move up
+{!info!}'-'{!normal!} - {|indent_pos:|}Move down
+
+{!info!}'Home'{!normal!} - {|indent_pos:|}Move to top
+{!info!}'End'{!normal!} - {|indent_pos:|}Move to bottom
+
+"""
+SEARCH_EMPTY = 0
+SEARCH_FAILING = 1
+SEARCH_SUCCESS = 2
+SEARCH_START_REACHED = 3
+SEARCH_END_REACHED = 4
+SEARCH_FORMAT = {
+ SEARCH_EMPTY: '{!black,white!}Search torrents: %s{!black,white!}',
+ SEARCH_SUCCESS: '{!black,white!}Search torrents: {!black,green!}%s{!black,white!}',
+ SEARCH_FAILING: '{!black,white!}Search torrents: {!black,red!}%s{!black,white!}',
+ SEARCH_START_REACHED: '{!black,white!}Search torrents: {!black,yellow!}%s{!black,white!} (start reached)',
+ SEARCH_END_REACHED: '{!black,white!}Search torrents: {!black,yellow!}%s{!black,white!} (end reached)',
+}
+
+
+class SearchMode(InputKeyHandler):
+ def __init__(self, torrentlist):
+ super(SearchMode, self).__init__()
+ self.torrentlist = torrentlist
+ self.torrentview = torrentlist.torrentview
+ self.search_state = SEARCH_EMPTY
+ self.search_string = ''
+
+ def update_cursor(self):
+ util.safe_curs_set(util.Curser.VERY_VISIBLE)
+ move_cursor(
+ self.torrentlist.stdscr,
+ self.torrentlist.rows - 1,
+ len(self.search_string) + 17,
+ )
+
+ def set_statusbar_args(self, statusbar_args):
+ statusbar_args['bottombar'] = (
+ SEARCH_FORMAT[self.search_state] % self.search_string
+ )
+ statusbar_args['bottombar_help'] = False
+
+ def update_colors(self, tidx, colors):
+ if len(self.search_string) > 1:
+ lcase_name = self.torrentview.torrent_names[tidx].lower()
+ sstring_lower = self.search_string.lower()
+ if lcase_name.find(sstring_lower) != -1:
+ if tidx == self.torrentview.cursel:
+ pass
+ elif tidx in self.torrentview.marked:
+ colors['bg'] = 'magenta'
+ else:
+ colors['bg'] = 'green'
+ if colors['fg'] == 'green':
+ colors['fg'] = 'black'
+ colors['attr'] = 'bold'
+
+ def do_search(self, direction='first'):
+ """
+ Performs a search on visible torrent and sets cursor to the match
+
+ Args:
+ direction (str): The direction to search. Must be one of 'first', 'last', 'next' or 'previous'
+
+ """
+ search_space = list(enumerate(self.torrentview.torrent_names))
+
+ if direction == 'last':
+ search_space = reversed(search_space)
+ elif direction == 'next':
+ search_space = search_space[self.torrentview.cursel + 1 :]
+ elif direction == 'previous':
+ search_space = reversed(search_space[: self.torrentview.cursel])
+
+ search_string = self.search_string.lower()
+ for i, n in search_space:
+ n = n.lower()
+ if n.find(search_string) != -1:
+ self.torrentview.cursel = i
+ if (
+ self.torrentview.curoff
+ + self.torrentview.torrent_rows
+ - self.torrentview.torrentlist_offset
+ ) < self.torrentview.cursel:
+ self.torrentview.curoff = (
+ self.torrentview.cursel - self.torrentview.torrent_rows + 1
+ )
+ elif (self.torrentview.curoff + 1) > self.torrentview.cursel:
+ self.torrentview.curoff = max(0, self.torrentview.cursel)
+ self.search_state = SEARCH_SUCCESS
+ return
+ if direction in ['first', 'last']:
+ self.search_state = SEARCH_FAILING
+ elif direction == 'next':
+ self.search_state = SEARCH_END_REACHED
+ elif direction == 'previous':
+ self.search_state = SEARCH_START_REACHED
+
+ @overrides(InputKeyHandler)
+ def handle_read(self, c):
+ cname = self.torrentview.torrent_names[self.torrentview.cursel]
+ refresh = True
+
+ if c in [
+ util.KEY_ESC,
+ util.KEY_BELL,
+ ]: # If Escape key or CTRL-g, we abort search
+ self.torrentlist.set_minor_mode(None)
+ self.search_state = SEARCH_EMPTY
+ elif c in [curses.KEY_BACKSPACE, util.KEY_BACKSPACE2]:
+ if self.search_string:
+ self.search_string = self.search_string[:-1]
+ if cname.lower().find(self.search_string.lower()) != -1:
+ self.search_state = SEARCH_SUCCESS
+ else:
+ self.torrentlist.set_minor_mode(None)
+ self.search_state = SEARCH_EMPTY
+ elif c == curses.KEY_DC:
+ self.search_string = ''
+ self.search_state = SEARCH_SUCCESS
+ elif c == curses.KEY_UP:
+ self.do_search('previous')
+ elif c == curses.KEY_DOWN:
+ self.do_search('next')
+ elif c == curses.KEY_LEFT:
+ self.torrentlist.set_minor_mode(None)
+ self.search_state = SEARCH_EMPTY
+ elif c == ord('/'):
+ self.torrentlist.set_minor_mode(None)
+ self.search_state = SEARCH_EMPTY
+ elif c == curses.KEY_RIGHT:
+ tid = self.torrentview.current_torrent_id()
+ self.torrentlist.show_torrent_details(tid)
+ refresh = False
+ elif c == curses.KEY_HOME:
+ self.do_search('first')
+ elif c == curses.KEY_END:
+ self.do_search('last')
+ elif c in [10, curses.KEY_ENTER]:
+ self.last_mark = -1
+ tid = self.torrentview.current_torrent_id()
+ torrent_actions_popup(self.torrentlist, [tid], details=True)
+ refresh = False
+ elif c == util.KEY_ESC:
+ self.search_string = ''
+ self.search_state = SEARCH_EMPTY
+ elif c > 31 and c < 256:
+ old_search_string = self.search_string
+ stroke = chr(c)
+ uchar = '' if PY2 else stroke
+ while not uchar:
+ try:
+ uchar = stroke.decode(self.torrentlist.encoding)
+ except UnicodeDecodeError:
+ c = self.torrentlist.stdscr.getch()
+ stroke += chr(c)
+
+ if uchar:
+ self.search_string += uchar
+
+ still_matching = (
+ cname.lower().find(self.search_string.lower())
+ == cname.lower().find(old_search_string.lower())
+ and cname.lower().find(self.search_string.lower()) != -1
+ )
+
+ if self.search_string and not still_matching:
+ self.do_search()
+ elif self.search_string:
+ self.search_state = SEARCH_SUCCESS
+ else:
+ refresh = False
+
+ if not self.search_string:
+ self.search_state = SEARCH_EMPTY
+ refresh = True
+
+ if refresh:
+ self.torrentlist.refresh([])
+
+ return util.ReadState.READ
diff --git a/deluge/ui/console/modes/torrentlist/torrentactions.py b/deluge/ui/console/modes/torrentlist/torrentactions.py
new file mode 100644
index 0000000..f3cd395
--- /dev/null
+++ b/deluge/ui/console/modes/torrentlist/torrentactions.py
@@ -0,0 +1,276 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
+#
+# 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.
+#
+
+from __future__ import unicode_literals
+
+import logging
+import os
+
+from twisted.internet import defer
+
+import deluge.component as component
+from deluge.ui.client import client
+from deluge.ui.common import TORRENT_DATA_FIELD
+from deluge.ui.console.modes.torrentlist.queue_mode import QueueMode
+from deluge.ui.console.utils import colors
+from deluge.ui.console.utils.common import TORRENT_OPTIONS
+from deluge.ui.console.widgets.popup import InputPopup, MessagePopup, SelectablePopup
+
+from . import ACTION
+
+log = logging.getLogger(__name__)
+
+
+def action_error(error, mode):
+ mode.report_message('Error Occurred', error.getErrorMessage())
+ mode.refresh()
+
+
+def action_remove(mode=None, torrent_ids=None, **kwargs):
+ def do_remove(*args, **kwargs):
+ data = args[0] if args else None
+ if data is None or kwargs.get('close', False):
+ mode.pop_popup()
+ return True
+
+ mode.torrentview.clear_marked()
+ remove_data = data['remove_files']['value']
+
+ def on_removed_finished(errors):
+ if errors:
+ error_msgs = ''
+ for t_id, e_msg in errors:
+ error_msgs += 'Error removing torrent %s : %s\n' % (t_id, e_msg)
+ mode.report_message(
+ 'Error(s) occured when trying to delete torrent(s).', error_msgs
+ )
+ mode.refresh()
+
+ d = client.core.remove_torrents(torrent_ids, remove_data)
+ d.addCallback(on_removed_finished)
+ mode.pop_popup()
+
+ def got_status(status):
+ return (status['name'], status['state'])
+
+ callbacks = []
+ for tid in torrent_ids:
+ d = client.core.get_torrent_status(tid, ['name', 'state'])
+ callbacks.append(d.addCallback(got_status))
+
+ def remove_dialog(status):
+ status = [t_status[1] for t_status in status]
+
+ if len(torrent_ids) == 1:
+ rem_msg = '{!info!}Remove the following torrent?{!input!}'
+ else:
+ rem_msg = '{!info!}Remove the following %d torrents?{!input!}' % len(
+ torrent_ids
+ )
+
+ show_max = 6
+ for i, (name, state) in enumerate(status):
+ color = colors.state_color[state]
+ rem_msg += '\n %s* {!input!}%s' % (color, name)
+ if i == show_max - 1:
+ if i < len(status) - 1:
+ rem_msg += '\n {!red!}And %i more' % (len(status) - show_max)
+ break
+
+ popup = InputPopup(
+ mode,
+ '(Esc to cancel, Enter to remove)',
+ close_cb=do_remove,
+ border_off_west=1,
+ border_off_north=1,
+ )
+ popup.add_text(rem_msg)
+ popup.add_spaces(1)
+ popup.add_select_input(
+ 'remove_files',
+ '{!info!}Torrent files:',
+ ['Keep', 'Remove'],
+ [False, True],
+ False,
+ )
+ mode.push_popup(popup)
+
+ defer.DeferredList(callbacks).addCallback(remove_dialog)
+
+
+def action_torrent_info(mode=None, torrent_ids=None, **kwargs):
+ popup = MessagePopup(mode, 'Torrent options', 'Querying core, please wait...')
+ mode.push_popup(popup)
+ torrents = torrent_ids
+ options = {}
+
+ def _do_set_torrent_options(torrent_ids, result):
+ options = {}
+ for opt, val in result.items():
+ if val['value'] not in ['multiple', None]:
+ options[opt] = val['value']
+ client.core.set_torrent_options(torrent_ids, options)
+
+ def on_torrent_status(status):
+ for key in status:
+ if key not in options:
+ options[key] = status[key]
+ elif options[key] != status[key]:
+ options[key] = 'multiple'
+
+ def create_popup(status):
+ mode.pop_popup()
+
+ def cb(result, **kwargs):
+ if result is None:
+ return
+ _do_set_torrent_options(torrent_ids, result)
+ if kwargs.get('close', False):
+ mode.pop_popup()
+ return True
+
+ option_popup = InputPopup(
+ mode,
+ ' Set Torrent Options ',
+ close_cb=cb,
+ border_off_west=1,
+ border_off_north=1,
+ base_popup=kwargs.get('base_popup', None),
+ )
+ for field in TORRENT_OPTIONS:
+ caption = '{!info!}' + TORRENT_DATA_FIELD[field]['name']
+ value = options[field]
+ if isinstance(value, ''.__class__):
+ option_popup.add_text_input(field, caption, value)
+ elif isinstance(value, bool):
+ choices = (['Yes', 'No'], [True, False], [True, False].index(value))
+ option_popup.add_select_input(
+ field, caption, choices[0], choices[1], choices[2]
+ )
+ elif isinstance(value, float):
+ option_popup.add_float_spin_input(
+ field, caption, value=value, min_val=-1
+ )
+ elif isinstance(value, int):
+ option_popup.add_int_spin_input(field, caption, value=value, min_val=-1)
+
+ mode.push_popup(option_popup)
+
+ callbacks = []
+ for tid in torrents:
+ deferred = component.get('SessionProxy').get_torrent_status(
+ tid, list(TORRENT_OPTIONS)
+ )
+ callbacks.append(deferred.addCallback(on_torrent_status))
+
+ callbacks = defer.DeferredList(callbacks)
+ callbacks.addCallback(create_popup)
+
+
+def torrent_action(action, *args, **kwargs):
+ retval = False
+ torrent_ids = kwargs.get('torrent_ids', None)
+ mode = kwargs.get('mode', None)
+
+ if torrent_ids is None:
+ return
+
+ if action == ACTION.PAUSE:
+ log.debug('Pausing torrents: %s', torrent_ids)
+ client.core.pause_torrents(torrent_ids).addErrback(action_error, mode)
+ retval = True
+ elif action == ACTION.RESUME:
+ log.debug('Resuming torrents: %s', torrent_ids)
+ client.core.resume_torrents(torrent_ids).addErrback(action_error, mode)
+ retval = True
+ elif action == ACTION.QUEUE:
+ queue_mode = QueueMode(mode, torrent_ids)
+ queue_mode.popup(**kwargs)
+ elif action == ACTION.REMOVE:
+ action_remove(**kwargs)
+ retval = True
+ elif action == ACTION.MOVE_STORAGE:
+
+ def do_move(res, **kwargs):
+ if res is None or kwargs.get('close', False):
+ mode.pop_popup()
+ return True
+
+ if os.path.exists(res['path']['value']) and not os.path.isdir(
+ res['path']['value']
+ ):
+ mode.report_message(
+ 'Cannot Move Download Folder',
+ '{!error!}%s exists and is not a directory' % res['path']['value'],
+ )
+ else:
+ log.debug('Moving %s to: %s', torrent_ids, res['path']['value'])
+ client.core.move_storage(torrent_ids, res['path']['value']).addErrback(
+ action_error, mode
+ )
+
+ popup = InputPopup(
+ mode, 'Move Download Folder', close_cb=do_move, border_off_east=1
+ )
+ popup.add_text_input('path', 'Enter path to move to:', complete=True)
+ mode.push_popup(popup)
+ elif action == ACTION.RECHECK:
+ log.debug('Rechecking torrents: %s', torrent_ids)
+ client.core.force_recheck(torrent_ids).addErrback(action_error, mode)
+ retval = True
+ elif action == ACTION.REANNOUNCE:
+ log.debug('Reannouncing torrents: %s', torrent_ids)
+ client.core.force_reannounce(torrent_ids).addErrback(action_error, mode)
+ retval = True
+ elif action == ACTION.DETAILS:
+ log.debug('Torrent details')
+ tid = mode.torrentview.current_torrent_id()
+ if tid:
+ mode.show_torrent_details(tid)
+ else:
+ log.error('No current torrent in _torrentaction, this is a bug')
+ elif action == ACTION.TORRENT_OPTIONS:
+ action_torrent_info(**kwargs)
+
+ return retval
+
+
+# Creates the popup. mode is the calling mode, tids is a list of torrents to take action upon
+def torrent_actions_popup(mode, torrent_ids, details=False, action=None, close_cb=None):
+
+ if action is not None:
+ torrent_action(action, mode=mode, torrent_ids=torrent_ids)
+ return
+
+ popup = SelectablePopup(
+ mode,
+ 'Torrent Actions',
+ torrent_action,
+ cb_args={'mode': mode, 'torrent_ids': torrent_ids},
+ close_cb=close_cb,
+ border_off_north=1,
+ border_off_west=1,
+ border_off_east=1,
+ )
+ popup.add_line(ACTION.PAUSE, '_Pause')
+ popup.add_line(ACTION.RESUME, '_Resume')
+ if details:
+ popup.add_divider()
+ popup.add_line(ACTION.QUEUE, 'Queue')
+ popup.add_divider()
+ popup.add_line(ACTION.REANNOUNCE, '_Update Tracker')
+ popup.add_divider()
+ popup.add_line(ACTION.REMOVE, 'Remo_ve Torrent')
+ popup.add_line(ACTION.RECHECK, '_Force Recheck')
+ popup.add_line(ACTION.MOVE_STORAGE, '_Move Download Folder')
+ popup.add_divider()
+ if details:
+ popup.add_line(ACTION.DETAILS, 'Torrent _Details')
+ popup.add_line(ACTION.TORRENT_OPTIONS, 'Torrent _Options')
+ mode.push_popup(popup)
diff --git a/deluge/ui/console/modes/torrentlist/torrentlist.py b/deluge/ui/console/modes/torrentlist/torrentlist.py
new file mode 100644
index 0000000..a427d65
--- /dev/null
+++ b/deluge/ui/console/modes/torrentlist/torrentlist.py
@@ -0,0 +1,348 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
+#
+# 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.
+#
+
+from __future__ import unicode_literals
+
+import logging
+from collections import deque
+
+import deluge.component as component
+from deluge.component import Component
+from deluge.decorators import overrides
+from deluge.ui.client import client
+from deluge.ui.console.modes.basemode import BaseMode, mkwin
+from deluge.ui.console.modes.torrentlist import torrentview, torrentviewcolumns
+from deluge.ui.console.modes.torrentlist.add_torrents_popup import (
+ show_torrent_add_popup,
+)
+from deluge.ui.console.modes.torrentlist.filtersidebar import FilterSidebar
+from deluge.ui.console.modes.torrentlist.queue_mode import QueueMode
+from deluge.ui.console.modes.torrentlist.search_mode import SearchMode
+from deluge.ui.console.utils import curses_util as util
+from deluge.ui.console.widgets.popup import MessagePopup, PopupsHandler
+
+try:
+ import curses
+except ImportError:
+ pass
+
+log = logging.getLogger(__name__)
+
+
+# Big help string that gets displayed when the user hits 'h'
+HELP_STR = """
+This screen shows an overview of the current torrents Deluge is managing. \
+The currently selected torrent is indicated with a white background. \
+You can change the selected torrent using the up/down arrows or the \
+PgUp/PgDown keys. Home and End keys go to the first and last torrent \
+respectively.
+
+Operations can be performed on multiple torrents by marking them and \
+then hitting Enter. See below for the keys used to mark torrents.
+
+You can scroll a popup window that doesn't fit its content (like \
+this one) using the up/down arrows, PgUp/PgDown and Home/End keys.
+
+All popup windows can be closed/canceled by hitting the Esc key \
+or the 'q' key (does not work for dialogs like the add torrent dialog)
+
+The actions you can perform and the keys to perform them are as follows:
+
+{!info!}'h'{!normal!} - {|indent_pos:|}Show this help
+{!info!}'p'{!normal!} - {|indent_pos:|}Open preferences
+{!info!}'l'{!normal!} - {|indent_pos:|}Enter Command Line mode
+{!info!}'e'{!normal!} - {|indent_pos:|}Show the event log view ({!info!}'q'{!normal!} to go back to overview)
+
+{!info!}'a'{!normal!} - {|indent_pos:|}Add a torrent
+{!info!}Delete{!normal!} - {|indent_pos:|}Delete a torrent
+
+{!info!}'/'{!normal!} - {|indent_pos:|}Search torrent names. \
+Searching starts immediately - matching torrents are highlighted in \
+green, you can cycle through them with Up/Down arrows and Home/End keys \
+You can view torrent details with right arrow, open action popup with \
+Enter key and exit search mode with '/' key, left arrow or \
+backspace with empty search field
+
+{!info!}'f'{!normal!} - {|indent_pos:|}Show only torrents in a certain state
+ (Will open a popup where you can select the state you want to see)
+{!info!}'q'{!normal!} - {|indent_pos:|}Enter queue mode
+
+{!info!}'S'{!normal!} - {|indent_pos:|}Show or hide the sidebar
+
+{!info!}Enter{!normal!} - {|indent_pos:|}Show torrent actions popup. Here you can do things like \
+pause/resume, remove, recheck and so on. These actions \
+apply to all currently marked torrents. The currently \
+selected torrent is automatically marked when you press enter.
+
+{!info!}'o'{!normal!} - {|indent_pos:|}Show and set torrent options - this will either apply \
+to all selected torrents(but not the highlighted one) or currently \
+selected torrent if nothing is selected
+
+{!info!}'Q'{!normal!} - {|indent_pos:|}quit deluge-console
+{!info!}'C'{!normal!} - {|indent_pos:|}show connection manager
+
+{!info!}'m'{!normal!} - {|indent_pos:|}Mark a torrent
+{!info!}'M'{!normal!} - {|indent_pos:|}Mark all torrents between currently selected torrent and last marked torrent
+{!info!}'c'{!normal!} - {|indent_pos:|}Clear selection
+
+{!info!}'v'{!normal!} - {|indent_pos:|}Show a dialog which allows you to choose columns to display
+{!info!}'<' / '>'{!normal!} - {|indent_pos:|}Change column by which to sort torrents
+
+{!info!}Right Arrow{!normal!} - {|indent_pos:|}Torrent Detail Mode. This includes more detailed information \
+about the currently selected torrent, as well as a view of the \
+files in the torrent and the ability to set file priorities.
+
+{!info!}'q'/Esc{!normal!} - {|indent_pos:|}Close a popup (Note that 'q' does not work for dialogs \
+where you input something
+"""
+
+
+class TorrentList(BaseMode, PopupsHandler):
+ def __init__(self, stdscr, encoding=None):
+ BaseMode.__init__(
+ self, stdscr, encoding=encoding, do_refresh=False, depend=['SessionProxy']
+ )
+ PopupsHandler.__init__(self)
+ self.messages = deque()
+ self.last_mark = -1
+ self.go_top = False
+ self.minor_mode = None
+
+ self.consoleui = component.get('ConsoleUI')
+ self.coreconfig = self.consoleui.coreconfig
+ self.config = self.consoleui.config
+ self.sidebar = FilterSidebar(self, self.config)
+ self.torrentview_panel = mkwin(
+ curses.COLOR_GREEN,
+ curses.LINES - 1,
+ curses.COLS - self.sidebar.width,
+ 0,
+ self.sidebar.width,
+ )
+ self.torrentview = torrentview.TorrentView(self, self.config)
+
+ util.safe_curs_set(util.Curser.INVISIBLE)
+ self.stdscr.notimeout(0)
+
+ def torrentview_columns(self):
+ return self.torrentview_panel.getmaxyx()[1]
+
+ def on_config_changed(self):
+ self.config.save()
+ self.torrentview.on_config_changed()
+
+ def toggle_sidebar(self):
+ if self.config['torrentview']['show_sidebar']:
+ self.sidebar.show()
+ self.sidebar.resize_window(curses.LINES - 2, self.sidebar.width)
+ self.torrentview_panel.resize(
+ curses.LINES - 1, curses.COLS - self.sidebar.width
+ )
+ self.torrentview_panel.mvwin(0, self.sidebar.width)
+ else:
+ self.sidebar.hide()
+ self.torrentview_panel.resize(curses.LINES - 1, curses.COLS)
+ self.torrentview_panel.mvwin(0, 0)
+ self.torrentview.update_columns()
+ # After updating the columns widths, clear row cache to recreate them
+ self.torrentview.cached_rows.clear()
+ self.refresh()
+
+ @overrides(Component)
+ def start(self):
+ self.torrentview.on_config_changed()
+ self.toggle_sidebar()
+
+ if self.config['first_run']:
+ self.push_popup(
+ MessagePopup(self, 'Welcome to Deluge', HELP_STR, width_req=0.65)
+ )
+ self.config['first_run'] = False
+ self.config.save()
+
+ if client.connected():
+ self.torrentview.update(refresh=False)
+
+ @overrides(Component)
+ def update(self):
+ if self.mode_paused():
+ return
+
+ if client.connected():
+ self.torrentview.update(refresh=True)
+
+ @overrides(BaseMode)
+ def resume(self):
+ super(TorrentList, self).resume()
+
+ @overrides(BaseMode)
+ def on_resize(self, rows, cols):
+ BaseMode.on_resize(self, rows, cols)
+
+ if self.popup:
+ self.popup.handle_resize()
+
+ if not self.consoleui.is_active_mode(self):
+ return
+
+ self.toggle_sidebar()
+
+ def show_torrent_details(self, tid):
+ mode = self.consoleui.set_mode('TorrentDetail')
+ mode.update(tid)
+
+ def set_minor_mode(self, mode):
+ self.minor_mode = mode
+ self.refresh()
+
+ def _show_visible_columns_popup(self):
+ self.push_popup(torrentviewcolumns.TorrentViewColumns(self))
+
+ @overrides(BaseMode)
+ def refresh(self, lines=None):
+ # Something has requested we scroll to the top of the list
+ if self.go_top:
+ self.torrentview.cursel = 0
+ self.torrentview.curoff = 0
+ self.go_top = False
+
+ if not lines:
+ if not self.consoleui.is_active_mode(self):
+ return
+ self.stdscr.erase()
+
+ self.add_string(1, self.torrentview.column_string, scr=self.torrentview_panel)
+
+ # Update the status bars
+ statusbar_args = {'scr': self.stdscr, 'bottombar_help': True}
+ if self.torrentview.curr_filter is not None:
+ statusbar_args['topbar'] = '%s {!filterstatus!}Current filter: %s' % (
+ self.statusbars.topbar,
+ self.torrentview.curr_filter,
+ )
+
+ if self.minor_mode:
+ self.minor_mode.set_statusbar_args(statusbar_args)
+
+ self.draw_statusbars(**statusbar_args)
+
+ self.torrentview.update_torrents(lines)
+
+ if self.minor_mode:
+ self.minor_mode.update_cursor()
+ else:
+ util.safe_curs_set(util.Curser.INVISIBLE)
+
+ if not self.consoleui.is_active_mode(self):
+ return
+
+ self.stdscr.noutrefresh()
+ self.torrentview_panel.noutrefresh()
+
+ if not self.sidebar.hidden():
+ self.sidebar.refresh()
+
+ if self.popup:
+ self.popup.refresh()
+
+ curses.doupdate()
+
+ @overrides(BaseMode)
+ def read_input(self):
+ # Read the character
+ affected_lines = None
+ c = self.stdscr.getch()
+
+ # Either ESC or ALT+<some key>
+ if c == util.KEY_ESC:
+ n = self.stdscr.getch()
+ if n == -1: # Means it was the escape key
+ pass
+ else: # ALT+<some key>
+ c = [c, n]
+
+ if self.popup:
+ ret = self.popup.handle_read(c)
+ if self.popup and self.popup.closed():
+ self.pop_popup()
+ self.refresh()
+ return ret
+ if util.is_printable_chr(c):
+ if chr(c) == 'Q':
+ component.get('ConsoleUI').quit()
+ elif chr(c) == 'C':
+ self.consoleui.set_mode('ConnectionManager')
+ return
+ elif chr(c) == 'q':
+ self.torrentview.update_marked(self.torrentview.cursel)
+ self.set_minor_mode(
+ QueueMode(self, self.torrentview._selected_torrent_ids())
+ )
+ return
+ elif chr(c) == '/':
+ self.set_minor_mode(SearchMode(self))
+ return
+
+ if self.sidebar.has_focus() and c not in [curses.KEY_RIGHT]:
+ self.sidebar.handle_read(c)
+ self.refresh()
+ return
+
+ if self.torrentview.numtorrents < 0:
+ return
+ elif self.minor_mode:
+ self.minor_mode.handle_read(c)
+ return
+
+ affected_lines = None
+ # Hand off to torrentview
+ if self.torrentview.handle_read(c) == util.ReadState.CHANGED:
+ affected_lines = self.torrentview.get_input_result()
+
+ if c == curses.KEY_LEFT:
+ if not self.sidebar.has_focus():
+ self.sidebar.set_focused(True)
+ self.refresh()
+ return
+ elif c == curses.KEY_RIGHT:
+ if self.sidebar.has_focus():
+ self.sidebar.set_focused(False)
+ self.refresh()
+ return
+ # We enter a new mode for the selected torrent here
+ tid = self.torrentview.current_torrent_id()
+ if tid:
+ self.show_torrent_details(tid)
+ return
+
+ elif util.is_printable_chr(c):
+ if chr(c) == 'a':
+ show_torrent_add_popup(self)
+ elif chr(c) == 'v':
+ self._show_visible_columns_popup()
+ elif chr(c) == 'h':
+ self.push_popup(MessagePopup(self, 'Help', HELP_STR, width_req=0.65))
+ elif chr(c) == 'p':
+ mode = self.consoleui.set_mode('Preferences')
+ mode.load_config()
+ return
+ elif chr(c) == 'e':
+ self.consoleui.set_mode('EventView')
+ return
+ elif chr(c) == 'S':
+ self.config['torrentview']['show_sidebar'] = (
+ self.config['torrentview']['show_sidebar'] is False
+ )
+ self.config.save()
+ self.toggle_sidebar()
+ elif chr(c) == 'l':
+ self.consoleui.set_mode('CmdLine', refresh=True)
+ return
+
+ self.refresh(affected_lines)
diff --git a/deluge/ui/console/modes/torrentlist/torrentview.py b/deluge/ui/console/modes/torrentlist/torrentview.py
new file mode 100644
index 0000000..67de3e7
--- /dev/null
+++ b/deluge/ui/console/modes/torrentlist/torrentview.py
@@ -0,0 +1,517 @@
+# -*- coding: utf-8 -*-
+#
+# 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.
+#
+
+from __future__ import unicode_literals
+
+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(TorrentView, self).__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,
+ '%s%s' % (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_up(1)
+ elif c == ord('k'):
+ affected_lines = self._scroll_down(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
diff --git a/deluge/ui/console/modes/torrentlist/torrentviewcolumns.py b/deluge/ui/console/modes/torrentlist/torrentviewcolumns.py
new file mode 100644
index 0000000..9dff843
--- /dev/null
+++ b/deluge/ui/console/modes/torrentlist/torrentviewcolumns.py
@@ -0,0 +1,162 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 bendikro <bro.devel+deluge@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.
+#
+
+from __future__ import unicode_literals
+
+from deluge.decorators import overrides
+from deluge.ui.console.utils import curses_util as util
+from deluge.ui.console.utils.column import torrent_data_fields
+from deluge.ui.console.widgets.fields import CheckedPlusInput, IntSpinInput
+from deluge.ui.console.widgets.popup import InputPopup, MessagePopup
+
+COLUMN_VIEW_HELP_STR = """
+Control column visibilty with the following actions:
+
+{!info!}'+'{!normal!} - {|indent_pos:|}Increase column width
+{!info!}'-'{!normal!} - {|indent_pos:|}Decrease column width
+
+{!info!}'CTRL+up'{!normal!} - {|indent_pos:|} Move column left
+{!info!}'CTRL+down'{!normal!} - {|indent_pos:|} Move column right
+"""
+
+column_pref_names = [
+ 'queue',
+ 'name',
+ 'size',
+ 'downloaded',
+ 'uploaded',
+ 'remaining',
+ 'state',
+ 'progress',
+ 'seeds',
+ 'peers',
+ 'seeds_peers_ratio',
+ 'download_speed',
+ 'upload_speed',
+ 'max_download_speed',
+ 'max_upload_speed',
+ 'eta',
+ 'ratio',
+ 'avail',
+ 'time_added',
+ 'completed_time',
+ 'last_seen_complete',
+ 'tracker',
+ 'download_location',
+ 'active_time',
+ 'seeding_time',
+ 'finished_time',
+ 'time_since_transfer',
+ 'shared',
+ 'owner',
+]
+
+
+class ColumnAndWidth(CheckedPlusInput):
+ def __init__(self, parent, name, message, child, on_width_func, **kwargs):
+ CheckedPlusInput.__init__(self, parent, name, message, child, **kwargs)
+ self.on_width_func = on_width_func
+
+ @overrides(CheckedPlusInput)
+ def handle_read(self, c):
+ if c in [ord('+'), ord('-')]:
+ val = self.child.get_value()
+ change = 1 if chr(c) == '+' else -1
+ self.child.set_value(val + change, validate=True)
+ self.on_width_func(self.name, self.child.get_value())
+ return util.ReadState.CHANGED
+ return CheckedPlusInput.handle_read(self, c)
+
+
+class TorrentViewColumns(InputPopup):
+ def __init__(self, torrentlist):
+ self.torrentlist = torrentlist
+ self.torrentview = torrentlist.torrentview
+
+ title = 'Visible columns (Esc to exit)'
+ InputPopup.__init__(
+ self,
+ torrentlist,
+ title,
+ close_cb=self._do_set_column_visibility,
+ immediate_action=True,
+ height_req=len(column_pref_names) - 5,
+ width_req=max(len(col) for col in column_pref_names + [title]) + 14,
+ border_off_west=1,
+ allow_rearrange=True,
+ )
+
+ msg_fmt = '%-25s'
+ self.add_header((msg_fmt % _('Columns')) + ' ' + _('Width'), space_below=True)
+
+ for colpref_name in column_pref_names:
+ col = self.torrentview.config['torrentview']['columns'][colpref_name]
+ width_spin = IntSpinInput(
+ self,
+ colpref_name + '_ width',
+ '',
+ self.move,
+ col['width'],
+ min_val=-1,
+ max_val=99,
+ fmt='%2d',
+ )
+
+ def on_width_func(name, width):
+ self.torrentview.config['torrentview']['columns'][name]['width'] = width
+
+ self._add_input(
+ ColumnAndWidth(
+ self,
+ colpref_name,
+ torrent_data_fields[colpref_name]['name'],
+ width_spin,
+ on_width_func,
+ checked=col['visible'],
+ checked_char='*',
+ msg_fmt=msg_fmt,
+ show_usage_hints=False,
+ child_always_visible=True,
+ )
+ )
+
+ def _do_set_column_visibility(
+ self, data=None, state_changed=True, close=True, **kwargs
+ ):
+ if close:
+ self.torrentlist.pop_popup()
+ return
+ elif not state_changed:
+ return
+
+ for key, value in data.items():
+ self.torrentview.config['torrentview']['columns'][key]['visible'] = value[
+ 'value'
+ ]
+ self.torrentview.config['torrentview']['columns'][key]['order'] = value[
+ 'order'
+ ]
+
+ self.torrentview.config.save()
+ self.torrentview.on_config_changed()
+ self.torrentlist.refresh([])
+
+ @overrides(InputPopup)
+ def handle_read(self, c):
+ if c == ord('h'):
+ popup = MessagePopup(
+ self.torrentlist,
+ 'Help',
+ COLUMN_VIEW_HELP_STR,
+ width_req=70,
+ border_off_west=1,
+ )
+ self.torrentlist.push_popup(popup)
+ return util.ReadState.READ
+ return InputPopup.handle_read(self, c)