mirror of
https://github.com/codex-storage/deluge.git
synced 2025-02-28 11:00:33 +00:00
[Console] Rewrite of the console code
This commit is a rewrite of larger parts of the console code. The motivation behind the rewrite is to cleanup the code and reduce code duplication to make it easier to understand and modify, and allow any form of code reuse. Most changes are to the interactive console, but also to how the different modes (BaseMode subclasses) are used and set up. * Address [#2097] - Improve match_torrent search match: Instead of matching e.g. torrent name with name.startswith(pattern) now check for asterix at beginning and end of pattern and search with startswith, endswith or __contains__ according to the pattern. Various smaller fixes: * Add errback handler to connection failed * Fix cmd line console mixing str and unicode input * Fix handling delete backwards with ALT+Backspace * Fix handling resizing of message popups * Fix docs generation warnings * Lets not stop the reactor on exception in basemode.. * Markup for translation arg help strings * Main functionality improvements: - Add support for indentation in formatting code in popup messages (like help) - Add filter sidebar - Add ComboBox and UI language selection - Add columnsview to allow rearranging the torrentlist columns and changing column widths. - Removed Columns pane in preferences as columnsview.py is sufficient - Remove torrent info panel (short cut 'i') as the torrent detail view is sufficient * Cleanups and code restructuring - Made BaseModes subclass of Component - Rewrite of most of basic window/panel to allow easier code reuse - Implemented better handling of multple popups by stacking popups. This makes it easier to return to previous popup when opening multiple popups. * Refactured console code: - modes/ for the different modes - Renamed Legacy mode to CmdLine - Renamed alltorrent.py to torrentlist.py and split the code into - torrentlist/columnsview.py - torrentlist/torrentsview.py - torrentlist/search_mode.py (minor mode) - torrentlist/queue_mode.py (minor mode) - cmdline/ for cmd line commands - utils/ for utility files - widgets/ for reusable GUI widgets - fields.py: Base widgets like TextInput, SelectInput, ComboInput - popup.py: Popup windows - inputpane.py: The BaseInputPane used to manage multiple base widgets in a panel - window.py: The BaseWindow used by all panels needing a curses screen - sidebar.py: The Sidebar panel - statusbars.py: The statusbars - Moved option parsing code from main.py to parser.py
This commit is contained in:
parent
2f8b4732b4
commit
20bae1bf90
29
deluge/tests/test_ui_console_fields.py
Normal file
29
deluge/tests/test_ui_console_fields.py
Normal file
@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from twisted.trial import unittest
|
||||
|
||||
from deluge.ui.console.widgets.fields import TextInput
|
||||
|
||||
|
||||
class Parent(object):
|
||||
|
||||
def __init__(self):
|
||||
self.border_off_x = 1
|
||||
self.pane_width = 20
|
||||
|
||||
|
||||
class UICommonTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self): # NOQA
|
||||
self.parent = Parent()
|
||||
|
||||
def tearDown(self): # NOQA
|
||||
pass
|
||||
|
||||
def test_text_input(self):
|
||||
def move_func(self, r, c):
|
||||
self._cursor_row = r
|
||||
self._cursor_col = c
|
||||
|
||||
t = TextInput(self.parent, "name", "message", move_func, 20, "/text/field/file/path", complete=False)
|
||||
self.assertTrue(t) # Shut flake8 up (unused variable)
|
@ -21,7 +21,7 @@ from twisted.internet import defer
|
||||
import deluge
|
||||
import deluge.component as component
|
||||
import deluge.ui.console
|
||||
import deluge.ui.console.commands.quit
|
||||
import deluge.ui.console.cmdline.commands.quit
|
||||
import deluge.ui.console.main
|
||||
import deluge.ui.web.server
|
||||
from deluge.ui import ui_entry
|
||||
@ -332,7 +332,7 @@ class ConsoleUIWithDaemonBaseTestCase(UIWithDaemonBaseTestCase):
|
||||
|
||||
def set_up(self):
|
||||
# Avoid calling reactor.shutdown after commands are executed by main.exec_args()
|
||||
self.patch(deluge.ui.console.commands.quit, "reactor", common.ReactorOverride())
|
||||
self.patch(deluge.ui.console.cmdline.commands.quit, "reactor", common.ReactorOverride())
|
||||
return UIWithDaemonBaseTestCase.set_up(self)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
|
@ -44,6 +44,52 @@ STATE_TRANSLATION = {
|
||||
"Error": _("Error"),
|
||||
}
|
||||
|
||||
TORRENT_DATA_FIELD = {
|
||||
"queue": {"name": "#", "status": ["queue"]},
|
||||
"name": {"name": _("Name"), "status": ["state", "name"]},
|
||||
"progress_state": {"name": _("Progress"), "status": ["progress", "state"]},
|
||||
"state": {"name": _("State"), "status": ["state"]},
|
||||
"progress": {"name": _("Progress"), "status": ["progress"]},
|
||||
"size": {"name": _("Size"), "status": ["total_wanted"]},
|
||||
"downloaded": {"name": _("Downloaded"), "status": ["all_time_download"]},
|
||||
"uploaded": {"name": _("Uploaded"), "status": ["total_uploaded"]},
|
||||
"remaining": {"name": _("Remaining"), "status": ["total_remaining"]},
|
||||
"ratio": {"name": _("Ratio"), "status": ["ratio"]},
|
||||
"download_speed": {"name": _("Down Speed"), "status": ["download_payload_rate"]},
|
||||
"upload_speed": {"name": _("Up Speed"), "status": ["upload_payload_rate"]},
|
||||
"max_download_speed": {"name": _("Down Limit"), "status": ["max_download_speed"]},
|
||||
"max_upload_speed": {"name": _("Up Limit"), "status": ["max_upload_speed"]},
|
||||
"max_connections": {"name": _("Max Connections"), "status": ["max_connections"]},
|
||||
"max_upload_slots": {"name": _("Max Upload Slots"), "status": ["max_upload_slots"]},
|
||||
"peers": {"name": _("Peers"), "status": ["num_peers", "total_peers"]},
|
||||
"seeds": {"name": _("Seeds"), "status": ["num_seeds", "total_seeds"]},
|
||||
"avail": {"name": _("Avail"), "status": ["distributed_copies"]},
|
||||
"seeds_peers_ratio": {"name": _("Seeds:Peers"), "status": ["seeds_peers_ratio"]},
|
||||
"time_added": {"name": _("Added"), "status": ["time_added"]},
|
||||
"tracker": {"name": _("Tracker"), "status": ["tracker_host"]},
|
||||
"download_location": {"name": _("Download Folder"), "status": ["download_location"]},
|
||||
"seeding_time": {"name": _("Seeding Time"), "status": ["seeding_time"]},
|
||||
"active_time": {"name": _("Active Time"), "status": ["active_time"]},
|
||||
"finished_time": {"name": _("Finished Time"), "status": ["finished_time"]},
|
||||
"last_seen_complete": {"name": _("Complete Seen"), "status": ["last_seen_complete"]},
|
||||
"completed_time": {"name": _("Completed"), "status": ["completed_time"]},
|
||||
"eta": {"name": _("ETA"), "status": ["eta"]},
|
||||
"shared": {"name": _("Shared"), "status": ["shared"]},
|
||||
"prioritize_first_last": {"name": _("Prioritize First/Last"), "status": ["prioritize_first_last"]},
|
||||
"sequential_download": {"name": _("Sequential Download"), "status": ["sequential_download"]},
|
||||
"is_auto_managed": {"name": _("Auto Managed"), "status": ["is_auto_managed"]},
|
||||
"auto_managed": {"name": _("Auto Managed"), "status": ["auto_managed"]},
|
||||
"stop_at_ratio": {"name": _("Stop At Ratio"), "status": ["stop_at_ratio"]},
|
||||
"stop_ratio": {"name": _("Stop Ratio"), "status": ["stop_ratio"]},
|
||||
"remove_at_ratio": {"name": _("Remove At Ratio"), "status": ["remove_at_ratio"]},
|
||||
"move_completed": {"name": _("Move On Completed"), "status": ["move_completed"]},
|
||||
"move_completed_path": {"name": _("Move Completed Path"), "status": ["move_completed_path"]},
|
||||
"move_on_completed": {"name": _("Move On Completed"), "status": ["move_on_completed"]},
|
||||
"move_on_completed_path": {"name": _("Move On Completed Path"), "status": ["move_on_completed_path"]},
|
||||
"owner": {"name": _("Owner"), "status": ["owner"]}
|
||||
}
|
||||
|
||||
|
||||
TRACKER_STATUS_TRANSLATION = {
|
||||
"Error": _("Error"),
|
||||
"Warning": _("Warning"),
|
||||
@ -73,9 +119,13 @@ class TorrentInfo(object):
|
||||
log.debug("Attempting to open %s.", filename)
|
||||
with open(filename, "rb") as _file:
|
||||
self.__m_filedata = _file.read()
|
||||
except IOError as ex:
|
||||
log.warning("Unable to open %s: %s", filename, ex)
|
||||
raise ex
|
||||
try:
|
||||
self.__m_metadata = bencode.bdecode(self.__m_filedata)
|
||||
except bencode.BTFailure as ex:
|
||||
log.warning("Unable to open %s: %s", filename, ex)
|
||||
log.warning("Failed to decode %s: %s", filename, ex)
|
||||
raise ex
|
||||
|
||||
self.__m_info_hash = sha(bencode.bencode(self.__m_metadata["info"])).hexdigest()
|
||||
|
@ -9,15 +9,17 @@
|
||||
# See LICENSE for more details.
|
||||
#
|
||||
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import logging
|
||||
import shlex
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from deluge.common import windows_check
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console.colors import strip_colors
|
||||
from deluge.ui.console.parser import OptionParser, OptionParserError
|
||||
from deluge.ui.console.utils.colors import strip_colors
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -32,13 +34,13 @@ class Commander(object):
|
||||
print(strip_colors(line))
|
||||
|
||||
def do_command(self, cmd_line):
|
||||
"""Run a console command
|
||||
"""Run a console command.
|
||||
|
||||
Args:
|
||||
cmd_line (str): Console command
|
||||
cmd_line (str): Console command.
|
||||
|
||||
Returns:
|
||||
Deferred: A deferred that fires when command has been executed
|
||||
Deferred: A deferred that fires when the command has been executed.
|
||||
|
||||
"""
|
||||
options = self.parse_command(cmd_line)
|
||||
@ -46,14 +48,19 @@ class Commander(object):
|
||||
return self.exec_command(options)
|
||||
return defer.succeed(None)
|
||||
|
||||
def exit(self, status=0, msg=None):
|
||||
self._exit = True
|
||||
if msg:
|
||||
print(msg)
|
||||
|
||||
def parse_command(self, cmd_line):
|
||||
"""Parse a console command and process with argparse
|
||||
"""Parse a console command and process with argparse.
|
||||
|
||||
Args:
|
||||
cmd_line (str): Console command
|
||||
cmd_line (str): Console command.
|
||||
|
||||
Returns:
|
||||
argparse.Namespace: The parsed command
|
||||
argparse.Namespace: The parsed command.
|
||||
|
||||
"""
|
||||
if not cmd_line:
|
||||
@ -100,7 +107,9 @@ class Commander(object):
|
||||
import traceback
|
||||
self.write("%s" % traceback.format_exc())
|
||||
return
|
||||
except Exception as ex:
|
||||
except OptionParserError as ex:
|
||||
import traceback
|
||||
log.warn("Error parsing command '%s': %s", args, ex)
|
||||
self.write("{!error!} %s" % ex)
|
||||
parser.print_help()
|
||||
return
|
||||
@ -110,19 +119,18 @@ class Commander(object):
|
||||
return options
|
||||
|
||||
def exec_command(self, options, *args):
|
||||
"""
|
||||
Execute a console command.
|
||||
"""Execute a console command.
|
||||
|
||||
Args:
|
||||
options (argparse.Namespace): The command to execute
|
||||
options (argparse.Namespace): The command to execute.
|
||||
|
||||
Returns:
|
||||
Deferred: A deferred that fires when command has been executed
|
||||
Deferred: A deferred that fires when command has been executed.
|
||||
|
||||
"""
|
||||
try:
|
||||
ret = self._commands[options.command].handle(options)
|
||||
except Exception as ex:
|
||||
except Exception as ex: # pylint: disable=broad-except
|
||||
self.write("{!error!} %s" % ex)
|
||||
log.exception(ex)
|
||||
import traceback
|
||||
@ -130,3 +138,59 @@ class Commander(object):
|
||||
return defer.succeed(True)
|
||||
else:
|
||||
return ret
|
||||
|
||||
|
||||
class BaseCommand(object):
|
||||
|
||||
usage = None
|
||||
interactive_only = False
|
||||
aliases = []
|
||||
_name = "base"
|
||||
epilog = ""
|
||||
|
||||
def complete(self, text, *args):
|
||||
return []
|
||||
|
||||
def handle(self, options):
|
||||
pass
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def name_with_alias(self):
|
||||
return "/".join([self._name] + self.aliases)
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return self.__doc__
|
||||
|
||||
def split(self, text):
|
||||
if windows_check():
|
||||
text = text.replace("\\", "\\\\")
|
||||
result = shlex.split(text)
|
||||
for i, s in enumerate(result):
|
||||
result[i] = s.replace(r"\ ", " ")
|
||||
result = [s for s in result if s != ""]
|
||||
return result
|
||||
|
||||
def create_parser(self):
|
||||
opts = {"prog": self.name_with_alias, "description": self.__doc__, "epilog": self.epilog}
|
||||
if self.usage:
|
||||
opts["usage"] = self.usage
|
||||
parser = OptionParser(**opts)
|
||||
parser.add_argument(self.name, metavar="")
|
||||
parser.base_parser = parser
|
||||
self.add_arguments(parser)
|
||||
return parser
|
||||
|
||||
def add_subparser(self, subparsers):
|
||||
opts = {"prog": self.name_with_alias, "help": self.__doc__, "description": self.__doc__}
|
||||
if self.usage:
|
||||
opts["usage"] = self.usage
|
||||
parser = subparsers.add_parser(self.name, **opts)
|
||||
self.add_arguments(parser)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
pass
|
1
deluge/ui/console/cmdline/commands/__init__.py
Normal file
1
deluge/ui/console/cmdline/commands/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from deluge.ui.console.cmdline.command import BaseCommand # NOQA
|
@ -18,16 +18,17 @@ from twisted.internet import defer
|
||||
import deluge.common
|
||||
import deluge.component as component
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console.main import BaseCommand
|
||||
|
||||
from . import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Add torrents"""
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("-p", "--path", dest="path", help="download folder for torrent")
|
||||
parser.add_argument("-p", "--path", dest="path", help=_("download folder for torrent"))
|
||||
parser.add_argument("torrents", metavar="<torrent>", nargs="+",
|
||||
help="One or more torrent files, URLs or magnet URIs")
|
||||
help=_("One or more torrent files, URLs or magnet URIs"))
|
||||
|
||||
def handle(self, options):
|
||||
self.console = component.get("ConsoleUI")
|
@ -9,7 +9,8 @@
|
||||
|
||||
import deluge.component as component
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console.main import BaseCommand
|
||||
|
||||
from . import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
@ -13,9 +13,10 @@ import logging
|
||||
import tokenize
|
||||
|
||||
import deluge.component as component
|
||||
import deluge.ui.console.colors as colors
|
||||
import deluge.ui.console.utils.colors as colors
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console.main import BaseCommand
|
||||
|
||||
from . import BaseCommand
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -65,14 +66,14 @@ def simple_eval(source):
|
||||
class Command(BaseCommand):
|
||||
"""Show and set configuration values"""
|
||||
|
||||
usage = "config [--set <key> <value>] [<key> [<key>...] ]"
|
||||
usage = _("Usage: config [--set <key> <value>] [<key> [<key>...] ]")
|
||||
|
||||
def add_arguments(self, parser):
|
||||
set_group = parser.add_argument_group("setting a value")
|
||||
set_group.add_argument("-s", "--set", action="store", metavar="<key>", help="set value for this key")
|
||||
set_group.add_argument("values", metavar="<value>", nargs="+", help="Value to set")
|
||||
set_group.add_argument("-s", "--set", action="store", metavar="<key>", help=_("set value for this key"))
|
||||
set_group.add_argument("values", metavar="<value>", nargs="+", help=_("Value to set"))
|
||||
get_group = parser.add_argument_group("getting values")
|
||||
get_group.add_argument("keys", metavar="<keys>", nargs="*", help="one or more keys separated by space")
|
||||
get_group.add_argument("keys", metavar="<keys>", nargs="*", help=_("one or more keys separated by space"))
|
||||
|
||||
def handle(self, options):
|
||||
self.console = component.get("ConsoleUI")
|
@ -12,7 +12,8 @@ import logging
|
||||
|
||||
import deluge.component as component
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console.main import BaseCommand
|
||||
|
||||
from . import BaseCommand
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -20,12 +21,12 @@ log = logging.getLogger(__name__)
|
||||
class Command(BaseCommand):
|
||||
"""Connect to a new deluge server."""
|
||||
|
||||
usage = "Usage: connect <host[:port]> [<username>] [<password>]"
|
||||
usage = _("Usage: connect <host[:port]> [<username>] [<password>]")
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("host", help="host and port", metavar="<host[:port]>")
|
||||
parser.add_argument("username", help="Username", metavar="<username>", nargs="?", default="")
|
||||
parser.add_argument("password", help="Password", metavar="<password>", nargs="?", default="")
|
||||
parser.add_argument("host", help=_("Daemon host and port"), metavar="<host[:port]>")
|
||||
parser.add_argument("username", help=_("Username"), metavar="<username>", nargs="?", default="")
|
||||
parser.add_argument("password", help=_("Password"), metavar="<password>", nargs="?", default="")
|
||||
|
||||
def add_parser(self, subparsers):
|
||||
parser = subparsers.add_parser(self.name, help=self.__doc__, description=self.__doc__, prog="connect")
|
@ -12,14 +12,15 @@ from twisted.internet import defer
|
||||
|
||||
import deluge.component as component
|
||||
import deluge.log
|
||||
from deluge.ui.console.main import BaseCommand
|
||||
|
||||
from . import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Enable and disable debugging"""
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("state", metavar="<on|off>", choices=["on", "off"], help="The new state")
|
||||
parser.add_argument("state", metavar="<on|off>", choices=["on", "off"], help=_("The new state"))
|
||||
|
||||
def handle(self, options):
|
||||
if options.state == "on":
|
@ -10,8 +10,8 @@
|
||||
import logging
|
||||
|
||||
import deluge.component as component
|
||||
from deluge.ui.console.main import BaseCommand
|
||||
from deluge.ui.console.modes.alltorrents import AllTorrents
|
||||
|
||||
from . import BaseCommand
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -22,11 +22,6 @@ class Command(BaseCommand):
|
||||
|
||||
def handle(self, options):
|
||||
console = component.get("ConsoleUI")
|
||||
try:
|
||||
at = component.get("AllTorrents")
|
||||
except KeyError:
|
||||
at = AllTorrents(console.stdscr, console.encoding)
|
||||
|
||||
console.set_mode(at)
|
||||
at._go_top = True
|
||||
at = console.set_mode("TorrentList")
|
||||
at.go_top = True
|
||||
at.resume()
|
@ -10,7 +10,8 @@
|
||||
|
||||
import deluge.component as component
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console.main import BaseCommand
|
||||
|
||||
from . import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
@ -13,7 +13,8 @@ import logging
|
||||
from twisted.internet import defer
|
||||
|
||||
import deluge.component as component
|
||||
from deluge.ui.console.main import BaseCommand
|
||||
|
||||
from . import BaseCommand
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -22,7 +23,7 @@ class Command(BaseCommand):
|
||||
"""displays help on other commands"""
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("commands", metavar="<command>", nargs="*", help="One or more commands")
|
||||
parser.add_argument("commands", metavar="<command>", nargs="*", help=_("One or more commands"))
|
||||
|
||||
def handle(self, options):
|
||||
self.console = component.get("ConsoleUI")
|
@ -14,10 +14,11 @@ from os.path import sep as dirsep
|
||||
|
||||
import deluge.common as common
|
||||
import deluge.component as component
|
||||
import deluge.ui.console.colors as colors
|
||||
import deluge.ui.console.utils.colors as colors
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console.main import BaseCommand
|
||||
from deluge.ui.console.modes import format_utils
|
||||
from deluge.ui.console.utils import format_utils
|
||||
|
||||
from . import BaseCommand
|
||||
|
||||
strwidth = format_utils.strwidth
|
||||
|
||||
@ -103,16 +104,16 @@ class Command(BaseCommand):
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("-v", "--verbose", action="store_true", default=False, dest="verbose",
|
||||
help="Show more information per torrent.")
|
||||
help=_("Show more information per torrent."))
|
||||
parser.add_argument("-d", "--detailed", action="store_true", default=False, dest="detailed",
|
||||
help="Show more detailed information including files and peers.")
|
||||
help=_("Show more detailed information including files and peers."))
|
||||
parser.add_argument("-s", "--state", action="store", dest="state",
|
||||
help="Show torrents with state STATE: %s." % (", ".join(STATES)))
|
||||
help=_("Show torrents with state STATE: %s." % (", ".join(STATES))))
|
||||
parser.add_argument("--sort", action="store", type=str, default="", dest="sort", help=self.sort_help)
|
||||
parser.add_argument("--sort-reverse", action="store", type=str, default="", dest="sort_rev",
|
||||
help="Same as --sort but items are in reverse order.")
|
||||
help=_("Same as --sort but items are in reverse order."))
|
||||
parser.add_argument("torrent_ids", metavar="<torrent-id>", nargs="*",
|
||||
help="One or more torrent ids. If none is given, list all")
|
||||
help=_("One or more torrent ids. If none is given, list all"))
|
||||
|
||||
def add_subparser(self, subparsers):
|
||||
parser = subparsers.add_parser(self.name, prog=self.name, help=self.__doc__,
|
@ -8,11 +8,16 @@
|
||||
# See LICENSE for more details.
|
||||
#
|
||||
|
||||
import logging
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
import deluge.component as component
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console.main import BaseCommand
|
||||
|
||||
from . import BaseCommand
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
torrent_options = {
|
||||
"max_download_speed": float,
|
||||
@ -25,24 +30,24 @@ torrent_options = {
|
||||
"stop_at_ratio": bool,
|
||||
"stop_ratio": float,
|
||||
"remove_at_ratio": bool,
|
||||
"move_on_completed": bool,
|
||||
"move_on_completed_path": str
|
||||
"move_completed": bool,
|
||||
"move_completed_path": str
|
||||
}
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Show and manage per-torrent options"""
|
||||
|
||||
usage = "manage <torrent-id> [--set <key> <value>] [<key> [<key>...] ]"
|
||||
usage = _("Usage: manage <torrent-id> [--set <key> <value>] [<key> [<key>...] ]")
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("torrent", metavar="<torrent>",
|
||||
help="an expression matched against torrent ids and torrent names")
|
||||
help=_("an expression matched against torrent ids and torrent names"))
|
||||
set_group = parser.add_argument_group("setting a value")
|
||||
set_group.add_argument("-s", "--set", action="store", metavar="<key>", help="set value for this key")
|
||||
set_group.add_argument("values", metavar="<value>", nargs="+", help="Value to set")
|
||||
set_group.add_argument("-s", "--set", action="store", metavar="<key>", help=_("set value for this key"))
|
||||
set_group.add_argument("values", metavar="<value>", nargs="+", help=_("Value to set"))
|
||||
get_group = parser.add_argument_group("getting values")
|
||||
get_group.add_argument("keys", metavar="<keys>", nargs="*", help="one or more keys separated by space")
|
||||
get_group.add_argument("keys", metavar="<keys>", nargs="*", help=_("one or more keys separated by space"))
|
||||
|
||||
def handle(self, options):
|
||||
self.console = component.get("ConsoleUI")
|
||||
@ -99,21 +104,7 @@ class Command(BaseCommand):
|
||||
deferred.callback(True)
|
||||
|
||||
self.console.write("Setting %s to %s for torrents %s.." % (key, val, torrent_ids))
|
||||
|
||||
for tid in torrent_ids:
|
||||
if key == "move_on_completed_path":
|
||||
client.core.set_torrent_move_completed_path(tid, val).addCallback(on_set_config)
|
||||
elif key == "move_on_completed":
|
||||
client.core.set_torrent_move_completed(tid, val).addCallback(on_set_config)
|
||||
elif key == "is_auto_managed":
|
||||
client.core.set_torrent_auto_managed(tid, val).addCallback(on_set_config)
|
||||
elif key == "remove_at_ratio":
|
||||
client.core.set_torrent_remove_at_ratio(tid, val).addCallback(on_set_config)
|
||||
elif key == "prioritize_first_last":
|
||||
client.core.set_torrent_prioritize_first_last(tid, val).addCallback(on_set_config)
|
||||
else:
|
||||
client.core.set_torrent_options(torrent_ids, {key: val}).addCallback(on_set_config)
|
||||
break
|
||||
client.core.set_torrent_options(torrent_ids, {key: val}).addCallback(on_set_config)
|
||||
return deferred
|
||||
|
||||
def complete(self, line):
|
@ -12,7 +12,8 @@ import os.path
|
||||
|
||||
import deluge.component as component
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console.main import BaseCommand
|
||||
|
||||
from . import BaseCommand
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -21,8 +22,8 @@ class Command(BaseCommand):
|
||||
"""Move torrents' storage location"""
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("torrent_ids", metavar="<torrent-id>", nargs="+", help="One or more torrent ids")
|
||||
parser.add_argument("path", metavar="<path>", help="The path to move the torrents to")
|
||||
parser.add_argument("torrent_ids", metavar="<torrent-id>", nargs="+", help=_("One or more torrent ids"))
|
||||
parser.add_argument("path", metavar="<path>", help=_("The path to move the torrents to"))
|
||||
|
||||
def handle(self, options):
|
||||
self.console = component.get("ConsoleUI")
|
@ -10,7 +10,8 @@
|
||||
|
||||
import deluge.component as component
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console.main import BaseCommand
|
||||
|
||||
from . import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@ -19,7 +20,7 @@ class Command(BaseCommand):
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("torrent_ids", metavar="<torrent-id>", nargs="+",
|
||||
help="One or more torrent ids. '*' pauses all torrents")
|
||||
help=_("One or more torrent ids. '*' pauses all torrents"))
|
||||
|
||||
def handle(self, options):
|
||||
self.console = component.get("ConsoleUI")
|
@ -10,7 +10,8 @@
|
||||
import deluge.component as component
|
||||
import deluge.configmanager
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console.main import BaseCommand
|
||||
|
||||
from . import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@ -18,14 +19,14 @@ class Command(BaseCommand):
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("-l", "--list", action="store_true", default=False, dest="list",
|
||||
help="Lists available plugins")
|
||||
help=_("Lists available plugins"))
|
||||
parser.add_argument("-s", "--show", action="store_true", default=False, dest="show",
|
||||
help="Shows enabled plugins")
|
||||
parser.add_argument("-e", "--enable", dest="enable", nargs="+", help="Enables a plugin")
|
||||
parser.add_argument("-d", "--disable", dest="disable", nargs="+", help="Disables a plugin")
|
||||
help=_("Shows enabled plugins"))
|
||||
parser.add_argument("-e", "--enable", dest="enable", nargs="+", help=_("Enables a plugin"))
|
||||
parser.add_argument("-d", "--disable", dest="disable", nargs="+", help=_("Disables a plugin"))
|
||||
parser.add_argument("-r", "--reload", action="store_true", default=False, dest="reload",
|
||||
help="Reload list of available plugins")
|
||||
parser.add_argument("-i", "--install", help="Install a plugin from an .egg file")
|
||||
help=_("Reload list of available plugins"))
|
||||
parser.add_argument("-i", "--install", help=_("Install a plugin from an .egg file"))
|
||||
|
||||
def handle(self, options):
|
||||
self.console = component.get("ConsoleUI")
|
@ -11,7 +11,8 @@
|
||||
from twisted.internet import error, reactor
|
||||
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console.main import BaseCommand
|
||||
|
||||
from . import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
@ -9,7 +9,8 @@
|
||||
|
||||
import deluge.component as component
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console.main import BaseCommand
|
||||
|
||||
from . import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@ -17,7 +18,7 @@ class Command(BaseCommand):
|
||||
usage = "recheck [ * | <torrent-id> [<torrent-id> ...] ]"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("torrent_ids", metavar="<torrent-id>", nargs="+", help="One or more torrent ids")
|
||||
parser.add_argument("torrent_ids", metavar="<torrent-id>", nargs="+", help=_("One or more torrent ids"))
|
||||
|
||||
def handle(self, options):
|
||||
self.console = component.get("ConsoleUI")
|
@ -10,16 +10,17 @@
|
||||
|
||||
import deluge.component as component
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console.main import BaseCommand
|
||||
|
||||
from . import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Resume torrents"""
|
||||
usage = "resume [ * | <torrent-id> [<torrent-id> ...] ]"
|
||||
usage = _("Usage: resume [ * | <torrent-id> [<torrent-id> ...] ]")
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("torrent_ids", metavar="<torrent-id>", nargs="+",
|
||||
help="One or more torrent ids. '*' resumes all torrents")
|
||||
help=_("One or more torrent ids. '*' resumes all torrents"))
|
||||
|
||||
def handle(self, options):
|
||||
self.console = component.get("ConsoleUI")
|
@ -8,9 +8,14 @@
|
||||
# See LICENSE for more details.
|
||||
#
|
||||
|
||||
import logging
|
||||
|
||||
import deluge.component as component
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console.main import BaseCommand
|
||||
|
||||
from . import BaseCommand
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@ -18,14 +23,24 @@ class Command(BaseCommand):
|
||||
aliases = ["del"]
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("--remove_data", action="store_true", default=False, help="remove the torrent's data")
|
||||
parser.add_argument("torrent_ids", metavar="<torrent-id>", nargs="+", help="One or more torrent ids")
|
||||
parser.add_argument("--remove_data", action="store_true", default=False, help=_("remove the torrent's data"))
|
||||
parser.add_argument("-c", "--confirm", action="store_true", default=False,
|
||||
help=_("List the matching torrents without removing."))
|
||||
parser.add_argument("torrent_ids", metavar="<torrent-id>", nargs="+", help=_("One or more torrent ids"))
|
||||
|
||||
def handle(self, options):
|
||||
self.console = component.get("ConsoleUI")
|
||||
torrent_ids = []
|
||||
for arg in options.torrent_ids:
|
||||
torrent_ids.extend(self.console.match_torrent(arg))
|
||||
torrent_ids = self.console.match_torrents(options.torrent_ids)
|
||||
|
||||
if not options.confirm:
|
||||
self.console.write("{!info!}%d %s %s{!info!}" % (len(torrent_ids),
|
||||
_n("torrent", "torrents", len(torrent_ids)),
|
||||
_n("match", "matches", len(torrent_ids))))
|
||||
for t_id in torrent_ids:
|
||||
name = self.console.get_torrent_name(t_id)
|
||||
self.console.write("* %-50s (%s)" % (name, t_id))
|
||||
self.console.write(_("Confirm with -c to remove the listed torrents (Count: %d)") % len(torrent_ids))
|
||||
return
|
||||
|
||||
def on_removed_finished(errors):
|
||||
if errors:
|
||||
@ -33,6 +48,7 @@ class Command(BaseCommand):
|
||||
for t_id, e_msg in errors:
|
||||
self.console.write("Error removing torrent %s : %s" % (t_id, e_msg))
|
||||
|
||||
log.info("Removing %d torrents", len(torrent_ids))
|
||||
d = client.core.remove_torrents(torrent_ids, options.remove_data)
|
||||
d.addCallback(on_removed_finished)
|
||||
|
@ -14,7 +14,8 @@ from twisted.internet import defer
|
||||
import deluge.component as component
|
||||
from deluge.common import TORRENT_STATE, fspeed
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console.main import BaseCommand
|
||||
|
||||
from . import BaseCommand
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -24,10 +25,10 @@ class Command(BaseCommand):
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("-r", "--raw", action="store_true", default=False, dest="raw",
|
||||
help="Don't format upload/download rates in KiB/s \
|
||||
(useful for scripts that want to do their own parsing)")
|
||||
help=_("Don't format upload/download rates in KiB/s "
|
||||
"(useful for scripts that want to do their own parsing)"))
|
||||
parser.add_argument("-n", "--no-torrents", action="store_false", default=True, dest="show_torrents",
|
||||
help="Don't show torrent status (this will make the command a bit faster)")
|
||||
help=_("Don't show torrent status (this will make the command a bit faster)"))
|
||||
|
||||
def handle(self, options):
|
||||
self.console = component.get("ConsoleUI")
|
@ -10,7 +10,8 @@
|
||||
|
||||
import deluge.component as component
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console.main import BaseCommand
|
||||
|
||||
from . import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
@ -30,7 +30,7 @@ log = logging.getLogger(__name__)
|
||||
def load_commands(command_dir):
|
||||
|
||||
def get_command(name):
|
||||
return getattr(__import__('deluge.ui.console.commands.%s' % name, {}, {}, ['Command']), 'Command')()
|
||||
return getattr(__import__('deluge.ui.console.cmdline.commands.%s' % name, {}, {}, ['Command']), 'Command')()
|
||||
|
||||
try:
|
||||
commands = []
|
||||
@ -40,10 +40,10 @@ def load_commands(command_dir):
|
||||
if not (filename.endswith('.py') or filename.endswith('.pyc')):
|
||||
continue
|
||||
cmd = get_command(filename.split('.')[len(filename.split('.')) - 2])
|
||||
cmd._name = filename.split('.')[len(filename.split('.')) - 2]
|
||||
names = [cmd._name]
|
||||
names.extend(cmd.aliases)
|
||||
for a in names:
|
||||
aliases = [filename.split('.')[len(filename.split('.')) - 2]]
|
||||
cmd._name = aliases[0]
|
||||
aliases.extend(cmd.aliases)
|
||||
for a in aliases:
|
||||
commands.append((a, cmd))
|
||||
return dict(commands)
|
||||
except OSError:
|
||||
@ -67,8 +67,9 @@ class Console(UI):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Console, self).__init__("console", *args, log_stream=LogStream(), **kwargs)
|
||||
|
||||
group = self.parser.add_argument_group(_("Console Options"), "These daemon connect options will be "
|
||||
"used for commands, or if console ui autoconnect is enabled.")
|
||||
group = self.parser.add_argument_group(_("Console Options"),
|
||||
_("These daemon connect options will be "
|
||||
"used for commands, or if console ui autoconnect is enabled."))
|
||||
group.add_argument("-d", "--daemon", dest="daemon_addr", required=False, default="127.0.0.1")
|
||||
group.add_argument("-p", "--port", dest="daemon_port", type=int, required=False, default="58846")
|
||||
group.add_argument("-U", "--username", dest="daemon_user", required=False)
|
||||
@ -76,17 +77,17 @@ class Console(UI):
|
||||
|
||||
# To properly print help message for the console commands ( e.g. deluge-console info -h),
|
||||
# we add a subparser for each command which will trigger the help/usage when given
|
||||
from deluge.ui.console.main import ConsoleCommandParser # import here because (see top)
|
||||
from deluge.ui.console.parser import ConsoleCommandParser # import here because (see top)
|
||||
self.console_parser = ConsoleCommandParser(parents=[self.parser], add_help=False, prog=self.parser.prog,
|
||||
description="Starts the Deluge console interface",
|
||||
formatter_class=lambda prog:
|
||||
DelugeTextHelpFormatter(prog, max_help_position=33, width=90))
|
||||
self.parser.subparser = self.console_parser
|
||||
self.console_parser.base_parser = self.parser
|
||||
subparsers = self.console_parser.add_subparsers(title="Console commands", help="Description", dest="command",
|
||||
description="The following console commands are available:",
|
||||
metavar="command")
|
||||
self.console_cmds = load_commands(os.path.join(UI_PATH, "commands"))
|
||||
subparsers = self.console_parser.add_subparsers(title=_("Console commands"), help=_("Description"),
|
||||
description=_("The following console commands are available:"),
|
||||
metavar=_("Command"), dest="command")
|
||||
self.console_cmds = load_commands(os.path.join(UI_PATH, "cmdline", "commands"))
|
||||
for c in sorted(self.console_cmds):
|
||||
self.console_cmds[c].add_subparser(subparsers)
|
||||
|
||||
|
@ -1,127 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# 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 time
|
||||
|
||||
import deluge.component as component
|
||||
from deluge.common import windows_check
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console import colors
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EventLog(component.Component):
|
||||
"""
|
||||
Prints out certain events as they are received from the core.
|
||||
"""
|
||||
def __init__(self):
|
||||
component.Component.__init__(self, "EventLog")
|
||||
self.console = component.get("ConsoleUI")
|
||||
self.prefix = "{!event!}* [%H:%M:%S] "
|
||||
self.date_change_format = "On {!yellow!}%a, %d %b %Y{!input!} %Z:"
|
||||
|
||||
client.register_event_handler("TorrentAddedEvent", self.on_torrent_added_event)
|
||||
client.register_event_handler("PreTorrentRemovedEvent", self.on_torrent_removed_event)
|
||||
client.register_event_handler("TorrentStateChangedEvent", self.on_torrent_state_changed_event)
|
||||
client.register_event_handler("TorrentFinishedEvent", self.on_torrent_finished_event)
|
||||
client.register_event_handler("NewVersionAvailableEvent", self.on_new_version_available_event)
|
||||
client.register_event_handler("SessionPausedEvent", self.on_session_paused_event)
|
||||
client.register_event_handler("SessionResumedEvent", self.on_session_resumed_event)
|
||||
client.register_event_handler("ConfigValueChangedEvent", self.on_config_value_changed_event)
|
||||
client.register_event_handler("PluginEnabledEvent", self.on_plugin_enabled_event)
|
||||
client.register_event_handler("PluginDisabledEvent", self.on_plugin_disabled_event)
|
||||
|
||||
self.previous_time = time.localtime(0)
|
||||
|
||||
def on_torrent_added_event(self, torrent_id, from_state):
|
||||
if from_state:
|
||||
return
|
||||
|
||||
def on_torrent_status(status):
|
||||
self.write("{!green!}Torrent Added: {!info!}%s ({!cyan!}%s{!info!})" % (
|
||||
status["name"], torrent_id))
|
||||
# Write out what state the added torrent took
|
||||
self.on_torrent_state_changed_event(torrent_id, status["state"])
|
||||
|
||||
client.core.get_torrent_status(torrent_id, ["name", "state"]).addCallback(on_torrent_status)
|
||||
|
||||
def on_torrent_removed_event(self, torrent_id):
|
||||
self.write("{!red!}Torrent Removed: {!info!}%s ({!cyan!}%s{!info!})" %
|
||||
(self.console.get_torrent_name(torrent_id), torrent_id))
|
||||
|
||||
def on_torrent_state_changed_event(self, torrent_id, state):
|
||||
# It's probably a new torrent, ignore it
|
||||
if not state:
|
||||
return
|
||||
# Modify the state string color
|
||||
if state in colors.state_color:
|
||||
state = colors.state_color[state] + state
|
||||
|
||||
t_name = self.console.get_torrent_name(torrent_id)
|
||||
|
||||
# Again, it's most likely a new torrent
|
||||
if not t_name:
|
||||
return
|
||||
|
||||
self.write("%s: {!info!}%s ({!cyan!}%s{!info!})" %
|
||||
(state, t_name, torrent_id))
|
||||
|
||||
def on_torrent_finished_event(self, torrent_id):
|
||||
if not windows_check() and component.get("AllTorrents").config["ring_bell"]:
|
||||
import curses.beep
|
||||
curses.beep()
|
||||
self.write("{!info!}Torrent Finished: %s ({!cyan!}%s{!info!})" %
|
||||
(self.console.get_torrent_name(torrent_id), torrent_id))
|
||||
|
||||
def on_new_version_available_event(self, version):
|
||||
self.write("{!input!}New Deluge version available: {!info!}%s" %
|
||||
(version))
|
||||
|
||||
def on_session_paused_event(self):
|
||||
self.write("{!input!}Session Paused")
|
||||
|
||||
def on_session_resumed_event(self):
|
||||
self.write("{!green!}Session Resumed")
|
||||
|
||||
def on_config_value_changed_event(self, key, value):
|
||||
color = "{!white,black,bold!}"
|
||||
try:
|
||||
color = colors.type_color[type(value)]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
self.write("ConfigValueChanged: {!input!}%s: %s%s" % (key, color, value))
|
||||
|
||||
def write(self, s):
|
||||
current_time = time.localtime()
|
||||
|
||||
date_different = False
|
||||
for field in ["tm_mday", "tm_mon", "tm_year"]:
|
||||
c = getattr(current_time, field)
|
||||
p = getattr(self.previous_time, field)
|
||||
if c != p:
|
||||
date_different = True
|
||||
|
||||
if date_different:
|
||||
string = time.strftime(self.date_change_format)
|
||||
self.console.write_event(" ")
|
||||
self.console.write_event(string)
|
||||
|
||||
p = time.strftime(self.prefix)
|
||||
|
||||
self.console.write_event(p + s)
|
||||
self.previous_time = current_time
|
||||
|
||||
def on_plugin_enabled_event(self, name):
|
||||
self.write("PluginEnabled: {!info!}%s" % name)
|
||||
|
||||
def on_plugin_disabled_event(self, name):
|
||||
self.write("PluginDisabled: {!info!}%s" % name)
|
@ -10,226 +10,83 @@
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import locale
|
||||
import logging
|
||||
import os
|
||||
import shlex
|
||||
import sys
|
||||
import time
|
||||
|
||||
from twisted.internet import defer, reactor
|
||||
|
||||
import deluge.common
|
||||
import deluge.component as component
|
||||
from deluge.configmanager import ConfigManager
|
||||
from deluge.decorators import overrides
|
||||
from deluge.error import DelugeError
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console import colors
|
||||
from deluge.ui.console.colors import ConsoleColorFormatter
|
||||
from deluge.ui.console.commander import Commander
|
||||
from deluge.ui.console.eventlog import EventLog
|
||||
from deluge.ui.console.statusbars import StatusBars
|
||||
from deluge.ui.console.modes.addtorrents import AddTorrents
|
||||
from deluge.ui.console.modes.basemode import TermResizeHandler
|
||||
from deluge.ui.console.modes.cmdline import CmdLine
|
||||
from deluge.ui.console.modes.eventview import EventView
|
||||
from deluge.ui.console.modes.preferences import Preferences
|
||||
from deluge.ui.console.modes.torrentdetail import TorrentDetail
|
||||
from deluge.ui.console.modes.torrentlist.torrentlist import TorrentList
|
||||
from deluge.ui.console.utils import colors
|
||||
from deluge.ui.console.widgets import StatusBars
|
||||
from deluge.ui.coreconfig import CoreConfig
|
||||
from deluge.ui.sessionproxy import SessionProxy
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConsoleBaseParser(argparse.ArgumentParser):
|
||||
|
||||
def format_help(self):
|
||||
"""
|
||||
Differs from ArgumentParser.format_help by adding the raw epilog
|
||||
as formatted in the string. Default bahavior mangles the formatting.
|
||||
|
||||
"""
|
||||
# Handle epilog manually to keep the text formatting
|
||||
epilog = self.epilog
|
||||
self.epilog = ""
|
||||
help_str = super(ConsoleBaseParser, self).format_help()
|
||||
if epilog is not None:
|
||||
help_str += epilog
|
||||
self.epilog = epilog
|
||||
return help_str
|
||||
DEFAULT_CONSOLE_PREFS = {
|
||||
"ring_bell": False,
|
||||
"first_run": True,
|
||||
"language": "",
|
||||
"torrentview": {
|
||||
"sort_primary": "queue",
|
||||
"sort_secondary": "name",
|
||||
"show_sidebar": True,
|
||||
"sidebar_width": 25,
|
||||
"separate_complete": True,
|
||||
"move_selection": True,
|
||||
"columns": {}
|
||||
},
|
||||
"addtorrents": {
|
||||
"show_misc_files": False, # TODO: Showing/hiding this
|
||||
"show_hidden_folders": False, # TODO: Showing/hiding this
|
||||
"sort_column": "date",
|
||||
"reverse_sort": True,
|
||||
"last_path": "~",
|
||||
},
|
||||
"cmdline": {
|
||||
"ignore_duplicate_lines": False,
|
||||
"third_tab_lists_all": False,
|
||||
"torrents_per_tab_press": 15,
|
||||
"save_command_history": True,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ConsoleCommandParser(ConsoleBaseParser):
|
||||
|
||||
def _split_args(self, args):
|
||||
command_options = []
|
||||
for a in args:
|
||||
if not a:
|
||||
continue
|
||||
if ";" in a:
|
||||
cmd_lines = [arg.strip() for arg in a.split(";")]
|
||||
elif " " in a:
|
||||
cmd_lines = [a]
|
||||
else:
|
||||
continue
|
||||
|
||||
for cmd_line in cmd_lines:
|
||||
cmds = shlex.split(cmd_line)
|
||||
cmd_options = super(ConsoleCommandParser, self).parse_args(args=cmds)
|
||||
cmd_options.command = cmds[0]
|
||||
command_options.append(cmd_options)
|
||||
|
||||
return command_options
|
||||
|
||||
def parse_args(self, args=None):
|
||||
"""Parse known UI args and handle common and process group options.
|
||||
|
||||
Notes:
|
||||
If started by deluge entry script this has already been done.
|
||||
|
||||
Args:
|
||||
args (list, optional): The arguments to parse.
|
||||
|
||||
Returns:
|
||||
argparse.Namespace: The parsed arguments.
|
||||
"""
|
||||
from deluge.ui.ui_entry import AMBIGUOUS_CMD_ARGS
|
||||
self.base_parser.parse_known_ui_args(args, withhold=AMBIGUOUS_CMD_ARGS)
|
||||
|
||||
multi_command = self._split_args(args)
|
||||
# If multiple commands were passed to console
|
||||
if multi_command:
|
||||
# With multiple commands, normal parsing will fail, so only parse
|
||||
# known arguments using the base parser, and then set
|
||||
# options.parsed_cmds to the already parsed commands
|
||||
options, remaining = self.base_parser.parse_known_args(args=args)
|
||||
options.parsed_cmds = multi_command
|
||||
else:
|
||||
subcommand = False
|
||||
if hasattr(self.base_parser, "subcommand"):
|
||||
subcommand = getattr(self.base_parser, "subcommand")
|
||||
if not subcommand:
|
||||
# We must use parse_known_args to handle case when no subcommand
|
||||
# is provided, because argparse does not support parsing without
|
||||
# a subcommand
|
||||
options, remaining = self.base_parser.parse_known_args(args=args)
|
||||
# If any options remain it means they do not exist. Reparse with
|
||||
# parse_args to trigger help message
|
||||
if remaining:
|
||||
options = self.base_parser.parse_args(args=args)
|
||||
options.parsed_cmds = []
|
||||
else:
|
||||
options = super(ConsoleCommandParser, self).parse_args(args=args)
|
||||
options.parsed_cmds = [options]
|
||||
|
||||
if not hasattr(options, "remaining"):
|
||||
options.remaining = []
|
||||
|
||||
return options
|
||||
|
||||
|
||||
class OptionParser(ConsoleBaseParser):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(OptionParser, self).__init__(**kwargs)
|
||||
self.formatter = ConsoleColorFormatter()
|
||||
|
||||
def exit(self, status=0, msg=None):
|
||||
self._exit = True
|
||||
if msg:
|
||||
print(msg)
|
||||
|
||||
def error(self, msg):
|
||||
"""error(msg : string)
|
||||
|
||||
Print a usage message incorporating 'msg' to stderr and exit.
|
||||
If you override this in a subclass, it should not return -- it
|
||||
should either exit or raise an exception.
|
||||
"""
|
||||
raise Exception(msg)
|
||||
|
||||
def print_usage(self, _file=None):
|
||||
console = component.get("ConsoleUI")
|
||||
if self.usage:
|
||||
for line in self.format_usage().splitlines():
|
||||
console.write(line)
|
||||
|
||||
def print_help(self, _file=None):
|
||||
console = component.get("ConsoleUI")
|
||||
console.set_batch_write(True)
|
||||
for line in self.format_help().splitlines():
|
||||
console.write(line)
|
||||
console.set_batch_write(False)
|
||||
|
||||
def format_help(self):
|
||||
"""Return help formatted with colors."""
|
||||
help_str = super(OptionParser, self).format_help()
|
||||
return self.formatter.format_colors(help_str)
|
||||
|
||||
|
||||
class BaseCommand(object):
|
||||
|
||||
usage = None
|
||||
interactive_only = False
|
||||
aliases = []
|
||||
_name = "base"
|
||||
epilog = ""
|
||||
|
||||
def complete(self, text, *args):
|
||||
return []
|
||||
|
||||
def handle(self, options):
|
||||
pass
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def name_with_alias(self):
|
||||
return "/".join([self._name] + self.aliases)
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return self.__doc__
|
||||
|
||||
def split(self, text):
|
||||
if deluge.common.windows_check():
|
||||
text = text.replace("\\", "\\\\")
|
||||
result = shlex.split(text)
|
||||
for i, s in enumerate(result):
|
||||
result[i] = s.replace(r"\ ", " ")
|
||||
result = [s for s in result if s != ""]
|
||||
return result
|
||||
|
||||
def create_parser(self):
|
||||
opts = {"prog": self.name_with_alias, "description": self.__doc__, "epilog": self.epilog}
|
||||
if self.usage:
|
||||
opts["usage"] = self.usage
|
||||
parser = OptionParser(**opts)
|
||||
parser.add_argument(self.name, metavar="")
|
||||
parser.base_parser = parser
|
||||
self.add_arguments(parser)
|
||||
return parser
|
||||
|
||||
def add_subparser(self, subparsers):
|
||||
opts = {"prog": self.name_with_alias, "help": self.__doc__, "description": self.__doc__}
|
||||
if self.usage:
|
||||
opts["usage"] = self.usage
|
||||
parser = subparsers.add_parser(self.name, **opts)
|
||||
self.add_arguments(parser)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
pass
|
||||
|
||||
|
||||
class ConsoleUI(component.Component):
|
||||
class ConsoleUI(component.Component, TermResizeHandler):
|
||||
|
||||
def __init__(self, options, cmds, log_stream):
|
||||
component.Component.__init__(self, "ConsoleUI", 2)
|
||||
component.Component.__init__(self, "ConsoleUI")
|
||||
TermResizeHandler.__init__(self)
|
||||
self.options = options
|
||||
self.log_stream = log_stream
|
||||
|
||||
# keep track of events for the log view
|
||||
self.events = []
|
||||
self.torrents = []
|
||||
self.statusbars = None
|
||||
self.modes = {}
|
||||
self.active_mode = None
|
||||
self.initialized = False
|
||||
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
self.encoding = locale.getpreferredencoding()
|
||||
except Exception:
|
||||
except locale.Error:
|
||||
self.encoding = sys.getdefaultencoding()
|
||||
|
||||
log.debug("Using encoding: %s", self.encoding)
|
||||
@ -284,6 +141,8 @@ Please use commands from the command line, e.g.:\n
|
||||
def flush(self):
|
||||
pass
|
||||
|
||||
# We don't ever want log output to terminal when running in
|
||||
# interactive mode, so insert a dummy here
|
||||
self.log_stream.out = ConsoleLog()
|
||||
|
||||
# Set Esc key delay to 0 to avoid a very annoying delay
|
||||
@ -297,6 +156,8 @@ Please use commands from the command line, e.g.:\n
|
||||
curses.wrapper(self.run)
|
||||
|
||||
def exec_args(self, options):
|
||||
"""Execute console commands from command line."""
|
||||
from deluge.ui.console.cmdline.command import Commander
|
||||
commander = Commander(self._commands)
|
||||
|
||||
def on_connect(result):
|
||||
@ -319,7 +180,7 @@ Please use commands from the command line, e.g.:\n
|
||||
# any of the commands.
|
||||
self.started_deferred.addCallback(on_started)
|
||||
return self.started_deferred
|
||||
d = component.start()
|
||||
d = self.start_console()
|
||||
d.addCallback(on_components_started)
|
||||
return d
|
||||
|
||||
@ -343,47 +204,161 @@ Please use commands from the command line, e.g.:\n
|
||||
return d
|
||||
|
||||
def run(self, stdscr):
|
||||
"""
|
||||
This method is called by the curses.wrapper to start the mainloop and
|
||||
screen.
|
||||
"""This method is called by the curses.wrapper to start the mainloop and screen.
|
||||
|
||||
:param stdscr: curses screen passed in from curses.wrapper
|
||||
Args:
|
||||
stdscr (_curses.curses window): curses screen passed in from curses.wrapper.
|
||||
|
||||
"""
|
||||
# We want to do an interactive session, so start up the curses screen and
|
||||
# pass it the function that handles commands
|
||||
colors.init_colors()
|
||||
self.stdscr = stdscr
|
||||
self.config = ConfigManager("console.conf", defaults=DEFAULT_CONSOLE_PREFS, file_version=2)
|
||||
self.config.run_converter((0, 1), 2, self._migrate_config_1_to_2)
|
||||
|
||||
self.statusbars = StatusBars()
|
||||
from deluge.ui.console.modes.connectionmanager import ConnectionManager
|
||||
self.stdscr = stdscr
|
||||
self.screen = ConnectionManager(stdscr, self.encoding)
|
||||
self.register_mode(ConnectionManager(stdscr, self.encoding), set_mode=True)
|
||||
|
||||
torrentlist = self.register_mode(TorrentList(self.stdscr, self.encoding))
|
||||
self.register_mode(CmdLine(self.stdscr, self.encoding))
|
||||
self.register_mode(EventView(torrentlist, self.stdscr, self.encoding))
|
||||
self.register_mode(TorrentDetail(torrentlist, self.stdscr, self.config, self.encoding))
|
||||
self.register_mode(Preferences(torrentlist, self.stdscr, self.config, self.encoding))
|
||||
self.register_mode(AddTorrents(torrentlist, self.stdscr, self.config, self.encoding))
|
||||
|
||||
self.eventlog = EventLog()
|
||||
|
||||
self.screen.topbar = "{!status!}Deluge " + deluge.common.get_version() + " Console"
|
||||
self.screen.bottombar = "{!status!}"
|
||||
self.screen.refresh()
|
||||
|
||||
# The Screen object is designed to run as a twisted reader so that it
|
||||
# can use twisted's select poll for non-blocking user input.
|
||||
reactor.addReader(self.screen)
|
||||
|
||||
self.active_mode.topbar = "{!status!}Deluge " + deluge.common.get_version() + " Console"
|
||||
self.active_mode.bottombar = "{!status!}"
|
||||
self.active_mode.refresh()
|
||||
# Start the twisted mainloop
|
||||
reactor.run()
|
||||
|
||||
def start(self):
|
||||
@overrides(TermResizeHandler)
|
||||
def on_terminal_size(self, *args):
|
||||
rows, cols = super(ConsoleUI, self).on_terminal_size(args)
|
||||
for mode in self.modes:
|
||||
self.modes[mode].on_resize(rows, cols)
|
||||
|
||||
def register_mode(self, mode, set_mode=False):
|
||||
self.modes[mode.mode_name] = mode
|
||||
if set_mode:
|
||||
self.set_mode(mode.mode_name)
|
||||
return mode
|
||||
|
||||
def set_mode(self, mode_name, refresh=False):
|
||||
log.debug("Setting console mode '%s'", mode_name)
|
||||
mode = self.modes.get(mode_name, None)
|
||||
if mode is None:
|
||||
log.error("Non-existent mode requested: '%s'", mode_name)
|
||||
return
|
||||
self.stdscr.erase()
|
||||
|
||||
if self.active_mode:
|
||||
self.active_mode.pause()
|
||||
d = component.pause([self.active_mode.mode_name])
|
||||
|
||||
def on_mode_paused(result, mode, *args):
|
||||
from deluge.ui.console.widgets.popup import PopupsHandler
|
||||
if isinstance(mode, PopupsHandler):
|
||||
if mode.popup is not None:
|
||||
# If popups are not removed, they are still referenced in the memory
|
||||
# which can cause issues as the popup's screen will not be destroyed.
|
||||
# This can lead to the popup border being visible for short periods
|
||||
# while the current modes' screen is repainted.
|
||||
log.error("Mode '%s' still has popups available after being paused."
|
||||
" Ensure all popups are removed on pause!", mode.popup.title)
|
||||
d.addCallback(on_mode_paused, self.active_mode)
|
||||
reactor.removeReader(self.active_mode)
|
||||
|
||||
self.active_mode = mode
|
||||
self.statusbars.screen = self.active_mode
|
||||
|
||||
# The Screen object is designed to run as a twisted reader so that it
|
||||
# can use twisted's select poll for non-blocking user input.
|
||||
reactor.addReader(self.active_mode)
|
||||
self.stdscr.clear()
|
||||
|
||||
if self.active_mode._component_state == "Stopped":
|
||||
component.start([self.active_mode.mode_name])
|
||||
else:
|
||||
component.resume([self.active_mode.mode_name])
|
||||
|
||||
mode.resume()
|
||||
if refresh:
|
||||
mode.refresh()
|
||||
return mode
|
||||
|
||||
def switch_mode(self, func, error_smg):
|
||||
def on_stop(arg):
|
||||
if arg and True in arg[0]:
|
||||
func()
|
||||
else:
|
||||
self.messages.append(("Error", error_smg))
|
||||
component.stop(["TorrentList"]).addCallback(on_stop)
|
||||
|
||||
def is_active_mode(self, mode):
|
||||
return mode == self.active_mode
|
||||
|
||||
def start_components(self):
|
||||
def on_started(result):
|
||||
component.pause(["TorrentList", "EventView", "AddTorrents", "TorrentDetail", "Preferences"])
|
||||
|
||||
if self.interactive:
|
||||
d = component.start().addCallback(on_started)
|
||||
else:
|
||||
d = component.start(["SessionProxy", "ConsoleUI", "CoreConfig"])
|
||||
return d
|
||||
|
||||
def start_console(self):
|
||||
# Maintain a list of (torrent_id, name) for use in tab completion
|
||||
self.torrents = []
|
||||
if not self.interactive:
|
||||
self.started_deferred = defer.Deferred()
|
||||
self.started_deferred = defer.Deferred()
|
||||
|
||||
def on_session_state(result):
|
||||
def on_torrents_status(torrents):
|
||||
for torrent_id, status in torrents.items():
|
||||
self.torrents.append((torrent_id, status["name"]))
|
||||
self.started_deferred.callback(True)
|
||||
if not self.initialized:
|
||||
self.initialized = True
|
||||
d = self.start_components()
|
||||
else:
|
||||
def on_stopped(result):
|
||||
return component.start(["SessionProxy"])
|
||||
d = component.stop(["SessionProxy"]).addCallback(on_stopped)
|
||||
return d
|
||||
|
||||
client.core.get_torrents_status({"id": result}, ["name"]).addCallback(on_torrents_status)
|
||||
client.core.get_session_state().addCallback(on_session_state)
|
||||
def start(self):
|
||||
def on_session_state(result):
|
||||
self.torrents = []
|
||||
self.events = []
|
||||
|
||||
def on_torrents_status(torrents):
|
||||
for torrent_id, status in torrents.items():
|
||||
self.torrents.append((torrent_id, status["name"]))
|
||||
self.started_deferred.callback(True)
|
||||
|
||||
client.core.get_torrents_status({"id": result}, ["name"]).addCallback(on_torrents_status)
|
||||
|
||||
d = client.core.get_session_state().addCallback(on_session_state)
|
||||
|
||||
# Register event handlers to keep the torrent list up-to-date
|
||||
client.register_event_handler("TorrentAddedEvent", self.on_torrent_added_event)
|
||||
client.register_event_handler("TorrentRemovedEvent", self.on_torrent_removed_event)
|
||||
return d
|
||||
|
||||
def on_torrent_added_event(self, event, from_state=False):
|
||||
def on_torrent_status(status):
|
||||
self.torrents.append((event, status["name"]))
|
||||
client.core.get_torrent_status(event, ["name"]).addCallback(on_torrent_status)
|
||||
|
||||
def on_torrent_removed_event(self, event):
|
||||
for index, (tid, name) in enumerate(self.torrents):
|
||||
if event == tid:
|
||||
del self.torrents[index]
|
||||
|
||||
def match_torrents(self, strings):
|
||||
torrent_ids = []
|
||||
for s in strings:
|
||||
torrent_ids.extend(self.match_torrent(s))
|
||||
return list(set(torrent_ids))
|
||||
|
||||
def match_torrent(self, string):
|
||||
"""
|
||||
@ -396,67 +371,235 @@ Please use commands from the command line, e.g.:\n
|
||||
no matches are found.
|
||||
|
||||
"""
|
||||
if self.interactive and isinstance(self.screen, deluge.ui.console.modes.legacy.Legacy):
|
||||
return self.screen.match_torrent(string)
|
||||
if not isinstance(string, unicode):
|
||||
string = unicode(string, self.encoding)
|
||||
|
||||
if string == "*" or string == "":
|
||||
return [tid for tid, name in self.torrents]
|
||||
|
||||
match_func = "__eq__"
|
||||
if string.startswith("*"):
|
||||
string = string[1:]
|
||||
match_func = "endswith"
|
||||
if string.endswith("*"):
|
||||
match_func = "__contains__" if match_func == "endswith" else "startswith"
|
||||
string = string[:-1]
|
||||
|
||||
matches = []
|
||||
|
||||
string = string.decode(self.encoding)
|
||||
for tid, name in self.torrents:
|
||||
if tid.startswith(string) or name.startswith(string):
|
||||
if not isinstance(name, unicode):
|
||||
name = unicode(name, self.encoding)
|
||||
if getattr(tid, match_func, None)(string) or getattr(name, match_func, None)(string):
|
||||
matches.append(tid)
|
||||
|
||||
return matches
|
||||
|
||||
def get_torrent_name(self, torrent_id):
|
||||
if self.interactive and hasattr(self.screen, "get_torrent_name"):
|
||||
return self.screen.get_torrent_name(torrent_id)
|
||||
|
||||
for tid, name in self.torrents:
|
||||
if torrent_id == tid:
|
||||
return name
|
||||
|
||||
return None
|
||||
|
||||
def set_batch_write(self, batch):
|
||||
if self.interactive and isinstance(self.screen, deluge.ui.console.modes.legacy.Legacy):
|
||||
return self.screen.set_batch_write(batch)
|
||||
if self.interactive and isinstance(self.active_mode, deluge.ui.console.modes.cmdline.CmdLine):
|
||||
return self.active_mode.set_batch_write(batch)
|
||||
|
||||
def tab_complete_torrent(self, line):
|
||||
if self.interactive and isinstance(self.screen, deluge.ui.console.modes.legacy.Legacy):
|
||||
return self.screen.tab_complete_torrent(line)
|
||||
if self.interactive and isinstance(self.active_mode, deluge.ui.console.modes.cmdline.CmdLine):
|
||||
return self.active_mode.tab_complete_torrent(line)
|
||||
|
||||
def tab_complete_path(self, line, path_type="file", ext="", sort="name", dirs_first=True):
|
||||
if self.interactive and isinstance(self.screen, deluge.ui.console.modes.legacy.Legacy):
|
||||
return self.screen.tab_complete_path(line, path_type=path_type, ext=ext, sort=sort, dirs_first=dirs_first)
|
||||
|
||||
def set_mode(self, mode):
|
||||
reactor.removeReader(self.screen)
|
||||
self.screen = mode
|
||||
self.statusbars.screen = self.screen
|
||||
reactor.addReader(self.screen)
|
||||
self.stdscr.clear()
|
||||
mode.refresh()
|
||||
if self.interactive and isinstance(self.active_mode, deluge.ui.console.modes.cmdline.CmdLine):
|
||||
return self.active_mode.tab_complete_path(line, path_type=path_type, ext=ext,
|
||||
sort=sort, dirs_first=dirs_first)
|
||||
|
||||
def on_client_disconnect(self):
|
||||
component.stop()
|
||||
|
||||
def write(self, s):
|
||||
if self.interactive:
|
||||
if isinstance(self.screen, deluge.ui.console.modes.legacy.Legacy):
|
||||
self.screen.write(s)
|
||||
if isinstance(self.active_mode, deluge.ui.console.modes.cmdline.CmdLine):
|
||||
self.active_mode.write(s)
|
||||
else:
|
||||
component.get("LegacyUI").add_line(s, False)
|
||||
component.get("CmdLine").add_line(s, False)
|
||||
self.events.append(s)
|
||||
else:
|
||||
print(colors.strip_colors(deluge.common.utf8_encoded(s)))
|
||||
|
||||
def write_event(self, s):
|
||||
if self.interactive:
|
||||
if isinstance(self.screen, deluge.ui.console.modes.legacy.Legacy):
|
||||
if isinstance(self.active_mode, deluge.ui.console.modes.cmdline.CmdLine):
|
||||
self.events.append(s)
|
||||
self.screen.write(s)
|
||||
self.active_mode.write(s)
|
||||
else:
|
||||
component.get("LegacyUI").add_line(s, False)
|
||||
component.get("CmdLine").add_line(s, False)
|
||||
self.events.append(s)
|
||||
else:
|
||||
print(colors.strip_colors(deluge.common.utf8_encoded(s)))
|
||||
|
||||
def _migrate_config_1_to_2(self, config):
|
||||
"""Create better structure by moving most settings out of dict root
|
||||
and into sub categories. Some keys are also renamed to be consistent
|
||||
with other UIs.
|
||||
"""
|
||||
def move_key(source, dest, source_key, dest_key=None):
|
||||
if dest_key is None:
|
||||
dest_key = source_key
|
||||
dest[dest_key] = source[source_key]
|
||||
del source[source_key]
|
||||
|
||||
# These are moved to 'torrentview' sub dict
|
||||
for k in ["sort_primary", "sort_secondary", "move_selection", "separate_complete"]:
|
||||
move_key(config, config["torrentview"], k)
|
||||
|
||||
# These are moved to 'addtorrents' sub dict
|
||||
for k in ["show_misc_files", "show_hidden_folders", "sort_column", "reverse_sort", "last_path"]:
|
||||
move_key(config, config["addtorrents"], "addtorrents_%s" % k, dest_key=k)
|
||||
|
||||
# These are moved to 'cmdline' sub dict
|
||||
for k in ["ignore_duplicate_lines", "torrents_per_tab_press", "third_tab_lists_all"]:
|
||||
move_key(config, config["cmdline"], k)
|
||||
|
||||
move_key(config, config["cmdline"], "save_legacy_history", dest_key="save_command_history")
|
||||
|
||||
# Add key for localization
|
||||
config["language"] = DEFAULT_CONSOLE_PREFS["language"]
|
||||
|
||||
# Migrate column settings
|
||||
columns = ["queue", "size", "state", "progress", "seeds", "peers", "downspeed", "upspeed",
|
||||
"eta", "ratio", "avail", "added", "tracker", "savepath", "downloaded", "uploaded",
|
||||
"remaining", "owner", "downloading_time", "seeding_time", "completed", "seeds_peers_ratio",
|
||||
"complete_seen", "down_limit", "up_limit", "shared", "name"]
|
||||
column_name_mapping = {
|
||||
"downspeed": "download_speed",
|
||||
"upspeed": "upload_speed",
|
||||
"added": "time_added",
|
||||
"savepath": "download_location",
|
||||
"completed": "completed_time",
|
||||
"complete_seen": "last_seen_complete",
|
||||
"down_limit": "max_download_speed",
|
||||
"up_limit": "max_upload_speed",
|
||||
"downloading_time": "active_time"
|
||||
}
|
||||
|
||||
from deluge.ui.console.modes.torrentlist.torrentview import default_columns
|
||||
# These are moved to 'torrentview.columns' sub dict
|
||||
for k in columns:
|
||||
column_name = column_name_mapping.get(k, k)
|
||||
config["torrentview"]["columns"][column_name] = {}
|
||||
if k == "name":
|
||||
config["torrentview"]["columns"][column_name]["visible"] = True
|
||||
else:
|
||||
move_key(config, config["torrentview"]["columns"][column_name], "show_%s" % k, dest_key="visible")
|
||||
move_key(config, config["torrentview"]["columns"][column_name], "%s_width" % k, dest_key="width")
|
||||
config["torrentview"]["columns"][column_name]["order"] = default_columns[column_name]["order"]
|
||||
|
||||
return config
|
||||
|
||||
|
||||
class EventLog(component.Component):
|
||||
"""
|
||||
Prints out certain events as they are received from the core.
|
||||
"""
|
||||
def __init__(self):
|
||||
component.Component.__init__(self, "EventLog")
|
||||
self.console = component.get("ConsoleUI")
|
||||
self.prefix = "{!event!}* [%H:%M:%S] "
|
||||
self.date_change_format = "On {!yellow!}%a, %d %b %Y{!input!} %Z:"
|
||||
|
||||
client.register_event_handler("TorrentAddedEvent", self.on_torrent_added_event)
|
||||
client.register_event_handler("PreTorrentRemovedEvent", self.on_torrent_removed_event)
|
||||
client.register_event_handler("TorrentStateChangedEvent", self.on_torrent_state_changed_event)
|
||||
client.register_event_handler("TorrentFinishedEvent", self.on_torrent_finished_event)
|
||||
client.register_event_handler("NewVersionAvailableEvent", self.on_new_version_available_event)
|
||||
client.register_event_handler("SessionPausedEvent", self.on_session_paused_event)
|
||||
client.register_event_handler("SessionResumedEvent", self.on_session_resumed_event)
|
||||
client.register_event_handler("ConfigValueChangedEvent", self.on_config_value_changed_event)
|
||||
client.register_event_handler("PluginEnabledEvent", self.on_plugin_enabled_event)
|
||||
client.register_event_handler("PluginDisabledEvent", self.on_plugin_disabled_event)
|
||||
|
||||
self.previous_time = time.localtime(0)
|
||||
|
||||
def on_torrent_added_event(self, torrent_id, from_state):
|
||||
if from_state:
|
||||
return
|
||||
|
||||
def on_torrent_status(status):
|
||||
self.write("{!green!}Torrent Added: {!info!}%s ({!cyan!}%s{!info!})" %
|
||||
(status["name"], torrent_id))
|
||||
# Write out what state the added torrent took
|
||||
self.on_torrent_state_changed_event(torrent_id, status["state"])
|
||||
|
||||
client.core.get_torrent_status(torrent_id, ["name", "state"]).addCallback(on_torrent_status)
|
||||
|
||||
def on_torrent_removed_event(self, torrent_id):
|
||||
self.write("{!red!}Torrent Removed: {!info!}%s ({!cyan!}%s{!info!})" %
|
||||
(self.console.get_torrent_name(torrent_id), torrent_id))
|
||||
|
||||
def on_torrent_state_changed_event(self, torrent_id, state):
|
||||
# It's probably a new torrent, ignore it
|
||||
if not state:
|
||||
return
|
||||
# Modify the state string color
|
||||
if state in colors.state_color:
|
||||
state = colors.state_color[state] + state
|
||||
|
||||
t_name = self.console.get_torrent_name(torrent_id)
|
||||
|
||||
# Again, it's most likely a new torrent
|
||||
if not t_name:
|
||||
return
|
||||
|
||||
self.write("%s: {!info!}%s ({!cyan!}%s{!info!})" %
|
||||
(state, t_name, torrent_id))
|
||||
|
||||
def on_torrent_finished_event(self, torrent_id):
|
||||
if not deluge.common.windows_check() and component.get("TorrentList").config["ring_bell"]:
|
||||
import curses.beep
|
||||
curses.beep()
|
||||
self.write("{!info!}Torrent Finished: %s ({!cyan!}%s{!info!})" %
|
||||
(self.console.get_torrent_name(torrent_id), torrent_id))
|
||||
|
||||
def on_new_version_available_event(self, version):
|
||||
self.write("{!input!}New Deluge version available: {!info!}%s" %
|
||||
(version))
|
||||
|
||||
def on_session_paused_event(self):
|
||||
self.write("{!input!}Session Paused")
|
||||
|
||||
def on_session_resumed_event(self):
|
||||
self.write("{!green!}Session Resumed")
|
||||
|
||||
def on_config_value_changed_event(self, key, value):
|
||||
color = "{!white,black,bold!}"
|
||||
try:
|
||||
color = colors.type_color[type(value)]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
self.write("ConfigValueChanged: {!input!}%s: %s%s" % (key, color, value))
|
||||
|
||||
def write(self, s):
|
||||
current_time = time.localtime()
|
||||
|
||||
date_different = False
|
||||
for field in ["tm_mday", "tm_mon", "tm_year"]:
|
||||
c = getattr(current_time, field)
|
||||
p = getattr(self.previous_time, field)
|
||||
if c != p:
|
||||
date_different = True
|
||||
|
||||
if date_different:
|
||||
string = time.strftime(self.date_change_format)
|
||||
self.console.write_event(" ")
|
||||
self.console.write_event(string)
|
||||
|
||||
p = time.strftime(self.prefix)
|
||||
|
||||
self.console.write_event(p + s)
|
||||
self.previous_time = current_time
|
||||
|
||||
def on_plugin_enabled_event(self, name):
|
||||
self.write("PluginEnabled: {!info!}%s" % name)
|
||||
|
||||
def on_plugin_disabled_event(self, name):
|
||||
self.write("PluginDisabled: {!info!}%s" % name)
|
||||
|
@ -13,11 +13,13 @@ import os
|
||||
|
||||
import deluge.common
|
||||
import deluge.component as component
|
||||
from deluge.decorators import overrides
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console.modes import format_utils
|
||||
from deluge.ui.console.modes.basemode import BaseMode
|
||||
from deluge.ui.console.modes.input_popup import InputPopup
|
||||
from deluge.ui.console.modes.popup import MessagePopup
|
||||
from deluge.ui.console.modes.torrentlist.add_torrents_popup import report_add_status
|
||||
from deluge.ui.console.utils import curses_util as util
|
||||
from deluge.ui.console.utils import format_utils
|
||||
from deluge.ui.console.widgets.popup import InputPopup, MessagePopup
|
||||
|
||||
try:
|
||||
import curses
|
||||
@ -55,21 +57,18 @@ if a file is highlighted
|
||||
"""
|
||||
|
||||
|
||||
class AddTorrents(BaseMode, component.Component):
|
||||
def __init__(self, alltorrentmode, stdscr, console_config, encoding=None):
|
||||
class AddTorrents(BaseMode):
|
||||
|
||||
def __init__(self, parent_mode, stdscr, console_config, encoding=None):
|
||||
self.console_config = console_config
|
||||
|
||||
self.alltorrentmode = alltorrentmode
|
||||
|
||||
self.parent_mode = parent_mode
|
||||
self.popup = None
|
||||
|
||||
self.view_offset = 0
|
||||
self.cursel = 0
|
||||
self.marked = set()
|
||||
self.last_mark = -1
|
||||
|
||||
path = os.path.expanduser(self.console_config["addtorrents_last_path"])
|
||||
path = os.path.expanduser(self.console_config["addtorrents"]["last_path"])
|
||||
|
||||
self.path_stack = ["/"] + path.strip("/").split("/")
|
||||
self.path_stack_pos = len(self.path_stack)
|
||||
@ -81,26 +80,22 @@ class AddTorrents(BaseMode, component.Component):
|
||||
self.raw_rows_dirs = []
|
||||
self.formatted_rows = []
|
||||
|
||||
self.sort_column = self.console_config["addtorrents_sort_column"]
|
||||
self.reverse_sort = self.console_config["addtorrents_reverse_sort"]
|
||||
self.sort_column = self.console_config["addtorrents"]["sort_column"]
|
||||
self.reverse_sort = self.console_config["addtorrents"]["reverse_sort"]
|
||||
|
||||
BaseMode.__init__(self, stdscr, encoding)
|
||||
|
||||
self._listing_space = self.rows - 5
|
||||
|
||||
self.__refresh_listing()
|
||||
|
||||
component.Component.__init__(self, "AddTorrents", 1, depend=["SessionProxy"])
|
||||
|
||||
component.start(["AddTorrents"])
|
||||
|
||||
curses.curs_set(0)
|
||||
util.safe_curs_set(util.Curser.INVISIBLE)
|
||||
self.stdscr.notimeout(0)
|
||||
|
||||
# component start/update
|
||||
@overrides(component.Component)
|
||||
def start(self):
|
||||
pass
|
||||
|
||||
@overrides(component.Component)
|
||||
def update(self):
|
||||
pass
|
||||
|
||||
@ -119,12 +114,12 @@ class AddTorrents(BaseMode, component.Component):
|
||||
|
||||
for f in listing:
|
||||
if os.path.isdir(os.path.join(path, f)):
|
||||
if self.console_config["addtorrents_show_hidden_folders"]:
|
||||
if self.console_config["addtorrents"]["show_hidden_folders"]:
|
||||
self.listing_dirs.append(f)
|
||||
elif f[0] != ".":
|
||||
self.listing_dirs.append(f)
|
||||
elif os.path.isfile(os.path.join(path, f)):
|
||||
if self.console_config["addtorrents_show_misc_files"]:
|
||||
if self.console_config["addtorrents"]["show_misc_files"]:
|
||||
self.listing_files.append(f)
|
||||
elif f.endswith(".torrent"):
|
||||
self.listing_files.append(f)
|
||||
@ -167,8 +162,8 @@ class AddTorrents(BaseMode, component.Component):
|
||||
self.__sort_rows()
|
||||
|
||||
def __sort_rows(self):
|
||||
self.console_config["addtorrents_sort_column"] = self.sort_column
|
||||
self.console_config["addtorrents_reverse_sort"] = self.reverse_sort
|
||||
self.console_config["addtorrents"]["sort_column"] = self.sort_column
|
||||
self.console_config["addtorrents"]["reverse_sort"] = self.reverse_sort
|
||||
self.console_config.save()
|
||||
|
||||
self.raw_rows_dirs.sort(key=lambda r: r[0].lower())
|
||||
@ -178,7 +173,6 @@ class AddTorrents(BaseMode, component.Component):
|
||||
elif self.sort_column == "date":
|
||||
self.raw_rows_files.sort(key=lambda r: r[2], reverse=self.reverse_sort)
|
||||
self.raw_rows = self.raw_rows_dirs + self.raw_rows_files
|
||||
|
||||
self.__refresh_rows()
|
||||
|
||||
def __refresh_rows(self):
|
||||
@ -224,37 +218,21 @@ class AddTorrents(BaseMode, component.Component):
|
||||
self.popup = pu
|
||||
self.refresh()
|
||||
|
||||
def on_resize(self, *args):
|
||||
BaseMode.on_resize_norefresh(self, *args)
|
||||
|
||||
# Always refresh Legacy(it will also refresh AllTorrents), otherwise it will bug deluge out
|
||||
legacy = component.get("LegacyUI")
|
||||
legacy.on_resize(*args)
|
||||
|
||||
@overrides(BaseMode)
|
||||
def on_resize(self, rows, cols):
|
||||
BaseMode.on_resize(self, rows, cols)
|
||||
if self.popup:
|
||||
self.popup.handle_resize()
|
||||
|
||||
self._listing_space = self.rows - 5
|
||||
|
||||
self.refresh()
|
||||
|
||||
def refresh(self, lines=None):
|
||||
if self.mode_paused():
|
||||
return
|
||||
|
||||
# Update the status bars
|
||||
self.stdscr.erase()
|
||||
self.add_string(0, self.statusbars.topbar)
|
||||
|
||||
# This will quite likely fail when switching modes
|
||||
try:
|
||||
rf = format_utils.remove_formatting
|
||||
string = self.statusbars.bottombar
|
||||
hstr = "Press {!magenta,blue,bold!}[h]{!status!} for help"
|
||||
|
||||
string += " " * (self.cols - len(rf(string)) - len(rf(hstr))) + hstr
|
||||
|
||||
self.add_string(self.rows - 1, string)
|
||||
except Exception as ex:
|
||||
log.debug("Exception caught: %s", ex)
|
||||
self.draw_statusbars()
|
||||
|
||||
off = 1
|
||||
|
||||
@ -331,7 +309,7 @@ class AddTorrents(BaseMode, component.Component):
|
||||
if off > self.rows - 2:
|
||||
break
|
||||
|
||||
if component.get("ConsoleUI").screen != self:
|
||||
if not component.get("ConsoleUI").is_active_mode(self):
|
||||
return
|
||||
|
||||
self.stdscr.noutrefresh()
|
||||
@ -342,12 +320,8 @@ class AddTorrents(BaseMode, component.Component):
|
||||
curses.doupdate()
|
||||
|
||||
def back_to_overview(self):
|
||||
component.stop(["AddTorrents"])
|
||||
component.deregister(self)
|
||||
self.stdscr.erase()
|
||||
component.get("ConsoleUI").set_mode(self.alltorrentmode)
|
||||
self.alltorrentmode._go_top = False
|
||||
self.alltorrentmode.resume()
|
||||
self.parent_mode.go_top = False
|
||||
component.get("ConsoleUI").set_mode(self.parent_mode.mode_name)
|
||||
|
||||
def _perform_action(self):
|
||||
if self.cursel < len(self.listing_dirs):
|
||||
@ -387,7 +361,7 @@ class AddTorrents(BaseMode, component.Component):
|
||||
|
||||
def _show_add_dialog(self):
|
||||
|
||||
def _do_add(result):
|
||||
def _do_add(result, **kwargs):
|
||||
ress = {"succ": 0, "fail": 0, "total": len(self.marked), "fmsg": []}
|
||||
|
||||
def fail_cb(msg, t_file, ress):
|
||||
@ -395,14 +369,14 @@ class AddTorrents(BaseMode, component.Component):
|
||||
ress["fail"] += 1
|
||||
ress["fmsg"].append("{!input!} * %s: {!error!}%s" % (t_file, msg))
|
||||
if (ress["succ"] + ress["fail"]) >= ress["total"]:
|
||||
self.alltorrentmode._report_add_status(ress["succ"], ress["fail"], ress["fmsg"])
|
||||
report_add_status(component.get("TorrentList"), ress["succ"], ress["fail"], ress["fmsg"])
|
||||
|
||||
def success_cb(tid, t_file, ress):
|
||||
if tid:
|
||||
log.debug("added torrent: %s (%s)", t_file, tid)
|
||||
ress["succ"] += 1
|
||||
if (ress["succ"] + ress["fail"]) >= ress["total"]:
|
||||
self.alltorrentmode._report_add_status(ress["succ"], ress["fail"], ress["fmsg"])
|
||||
report_add_status(component.get("TorrentList"), ress["succ"], ress["fail"], ress["fmsg"])
|
||||
else:
|
||||
fail_cb("Already in session (probably)", t_file, ress)
|
||||
|
||||
@ -413,21 +387,20 @@ class AddTorrents(BaseMode, component.Component):
|
||||
with open(path) as _file:
|
||||
filedump = base64.encodestring(_file.read())
|
||||
t_options = {}
|
||||
if result["location"]:
|
||||
t_options["download_location"] = result["location"]
|
||||
t_options["add_paused"] = result["add_paused"]
|
||||
if result["location"]["value"]:
|
||||
t_options["download_location"] = result["location"]["value"]
|
||||
t_options["add_paused"] = result["add_paused"]["value"]
|
||||
|
||||
d = client.core.add_torrent_file(filename, filedump, t_options)
|
||||
d.addCallback(success_cb, filename, ress)
|
||||
d.addErrback(fail_cb, filename, ress)
|
||||
|
||||
self.console_config["addtorrents_last_path"] = os.path.join(*self.path_stack[:self.path_stack_pos])
|
||||
self.console_config["addtorrents"]["last_path"] = os.path.join(*self.path_stack[:self.path_stack_pos])
|
||||
self.console_config.save()
|
||||
|
||||
self.back_to_overview()
|
||||
|
||||
config = component.get("ConsoleUI").coreconfig
|
||||
dl = config["download_location"]
|
||||
if config["add_paused"]:
|
||||
ap = 0
|
||||
else:
|
||||
@ -445,8 +418,8 @@ class AddTorrents(BaseMode, component.Component):
|
||||
self.popup.add_text(msg)
|
||||
self.popup.add_spaces(1)
|
||||
|
||||
self.popup.add_text_input("Download Folder:", "location", dl)
|
||||
self.popup.add_select_input("Add Paused:", "add_paused", ["Yes", "No"], [True, False], ap)
|
||||
self.popup.add_text_input("location", "Download Folder:", config["download_location"], complete=True)
|
||||
self.popup.add_select_input("add_paused", "Add Paused:", ["Yes", "No"], [True, False], ap)
|
||||
|
||||
def _go_up(self):
|
||||
# Go up in directory hierarchy
|
||||
@ -469,7 +442,7 @@ class AddTorrents(BaseMode, component.Component):
|
||||
self.refresh()
|
||||
return
|
||||
|
||||
if c > 31 and c < 256:
|
||||
if util.is_printable_char(c):
|
||||
if chr(c) == "Q":
|
||||
from twisted.internet import reactor
|
||||
if client.connected():
|
||||
@ -487,14 +460,12 @@ class AddTorrents(BaseMode, component.Component):
|
||||
if c == curses.KEY_UP:
|
||||
self.scroll_list_up(1)
|
||||
elif c == curses.KEY_PPAGE:
|
||||
# self.scroll_list_up(self._listing_space-2)
|
||||
self.scroll_list_up(self.rows // 2)
|
||||
elif c == curses.KEY_HOME:
|
||||
self.scroll_list_up(len(self.formatted_rows))
|
||||
elif c == curses.KEY_DOWN:
|
||||
self.scroll_list_down(1)
|
||||
elif c == curses.KEY_NPAGE:
|
||||
# self.scroll_list_down(self._listing_space-2)
|
||||
self.scroll_list_down(self.rows // 2)
|
||||
elif c == curses.KEY_END:
|
||||
self.scroll_list_down(len(self.formatted_rows))
|
||||
@ -503,14 +474,12 @@ class AddTorrents(BaseMode, component.Component):
|
||||
self._enter_dir()
|
||||
elif c == curses.KEY_LEFT:
|
||||
self._go_up()
|
||||
# Enter Key
|
||||
elif c == curses.KEY_ENTER or c == 10:
|
||||
elif c in [curses.KEY_ENTER, util.KEY_ENTER2]:
|
||||
self._perform_action()
|
||||
# Escape
|
||||
elif c == 27:
|
||||
elif c == util.KEY_ESC:
|
||||
self.back_to_overview()
|
||||
else:
|
||||
if c > 31 and c < 256:
|
||||
if util.is_printable_char(c):
|
||||
if chr(c) == "h":
|
||||
self.popup = MessagePopup(self, "Help", HELP_STR, width_req=0.75)
|
||||
elif chr(c) == ">":
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -11,13 +11,14 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from twisted.internet import reactor
|
||||
|
||||
import deluge.component as component
|
||||
import deluge.ui.console.colors as colors
|
||||
import deluge.ui.console.utils.colors as colors
|
||||
from deluge.ui.console.utils import curses_util as util
|
||||
from deluge.ui.console.utils.format_utils import remove_formatting
|
||||
|
||||
try:
|
||||
import curses
|
||||
import curses.panel
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@ -33,9 +34,53 @@ except ImportError:
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InputKeyHandler(object):
|
||||
|
||||
def __init__(self):
|
||||
self._input_result = None
|
||||
|
||||
def set_input_result(self, result):
|
||||
self._input_result = result
|
||||
|
||||
def get_input_result(self):
|
||||
result = self._input_result
|
||||
self._input_result = None
|
||||
return result
|
||||
|
||||
def handle_read(self, c):
|
||||
"""Handle a character read from curses screen
|
||||
|
||||
Returns:
|
||||
int: One of the constants defined in util.curses_util.ReadState.
|
||||
ReadState.IGNORED: The key was not handled. Further processing should continue.
|
||||
ReadState.READ: The key was read and processed. Do no further processing
|
||||
ReadState.CHANGED: The key was read and processed. Internal state was changed
|
||||
leaving data to be read by the caller.
|
||||
|
||||
"""
|
||||
return util.ReadState.IGNORED
|
||||
|
||||
|
||||
class TermResizeHandler(object):
|
||||
|
||||
def __init__(self):
|
||||
try:
|
||||
signal.signal(signal.SIGWINCH, self.on_terminal_size)
|
||||
except ValueError as ex:
|
||||
log.debug("Unable to catch SIGWINCH signal: %s", ex)
|
||||
|
||||
def on_terminal_size(self, *args):
|
||||
# Get the new rows and cols value
|
||||
rows, cols = struct.unpack("hhhh", ioctl(0, termios.TIOCGWINSZ, "\000" * 8))[0:2]
|
||||
curses.resizeterm(rows, cols)
|
||||
return rows, cols
|
||||
|
||||
|
||||
class CursesStdIO(object):
|
||||
"""fake fd to be registered as a reader with the twisted reactor.
|
||||
Curses classes needing input should extend this"""
|
||||
"""
|
||||
fake fd to be registered as a reader with the twisted reactor.
|
||||
Curses classes needing input should extend this
|
||||
"""
|
||||
|
||||
def fileno(self):
|
||||
""" We want to select on FD 0 """
|
||||
@ -49,8 +94,9 @@ class CursesStdIO(object):
|
||||
return "CursesClient"
|
||||
|
||||
|
||||
class BaseMode(CursesStdIO):
|
||||
def __init__(self, stdscr, encoding=None, do_refresh=True):
|
||||
class BaseMode(CursesStdIO, component.Component):
|
||||
|
||||
def __init__(self, stdscr, encoding=None, do_refresh=True, mode_name=None, depend=None):
|
||||
"""
|
||||
A mode that provides a curses screen designed to run as a reader in a twisted reactor.
|
||||
This mode doesn't do much, just shows status bars and "Base Mode" on the screen
|
||||
@ -71,111 +117,67 @@ class BaseMode(CursesStdIO):
|
||||
self.topbar - top statusbar
|
||||
self.bottombar - bottom statusbar
|
||||
"""
|
||||
log.debug("BaseMode init!")
|
||||
self.mode_name = mode_name if mode_name else self.__class__.__name__
|
||||
component.Component.__init__(self, self.mode_name, 1, depend=depend)
|
||||
self.stdscr = stdscr
|
||||
# Make the input calls non-blocking
|
||||
self.stdscr.nodelay(1)
|
||||
|
||||
self.paused = False
|
||||
# Strings for the 2 status bars
|
||||
self.statusbars = component.get("StatusBars")
|
||||
self.help_hstr = "{!status!} Press {!magenta,blue,bold!}[h]{!status!} for help"
|
||||
|
||||
# Keep track of the screen size
|
||||
self.rows, self.cols = self.stdscr.getmaxyx()
|
||||
try:
|
||||
signal.signal(signal.SIGWINCH, self.on_resize)
|
||||
except Exception:
|
||||
log.debug("Unable to catch SIGWINCH signal!")
|
||||
|
||||
if not encoding:
|
||||
self.encoding = sys.getdefaultencoding()
|
||||
else:
|
||||
self.encoding = encoding
|
||||
|
||||
colors.init_colors()
|
||||
|
||||
# Do a refresh right away to draw the screen
|
||||
if do_refresh:
|
||||
self.refresh()
|
||||
|
||||
def on_resize_norefresh(self, *args):
|
||||
log.debug("on_resize_from_signal")
|
||||
# Get the new rows and cols value
|
||||
self.rows, self.cols = struct.unpack("hhhh", ioctl(0, termios.TIOCGWINSZ, "\000" * 8))[0:2]
|
||||
curses.resizeterm(self.rows, self.cols)
|
||||
|
||||
def on_resize(self, *args):
|
||||
self.on_resize_norefresh(args)
|
||||
self.refresh()
|
||||
def on_resize(self, rows, cols):
|
||||
self.rows, self.cols = rows, cols
|
||||
|
||||
def connectionLost(self, reason): # NOQA
|
||||
self.close()
|
||||
|
||||
def add_string(self, row, string, scr=None, col=0, pad=True, trim=True):
|
||||
"""
|
||||
Adds a string to the desired `:param:row`.
|
||||
|
||||
:param row: int, the row number to write the string
|
||||
:param string: string, the string of text to add
|
||||
:param scr: curses.window, optional window to add string to instead of self.stdscr
|
||||
:param col: int, optional starting column offset
|
||||
:param pad: bool, optional bool if the string should be padded out to the width of the screen
|
||||
:param trim: bool, optional bool if the string should be trimmed if it is too wide for the screen
|
||||
|
||||
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!"
|
||||
|
||||
|
||||
"""
|
||||
def add_string(self, row, string, scr=None, **kwargs):
|
||||
if scr:
|
||||
screen = scr
|
||||
else:
|
||||
screen = self.stdscr
|
||||
try:
|
||||
parsed = colors.parse_color_string(string, self.encoding)
|
||||
except colors.BadColorString as ex:
|
||||
log.error("Cannot add bad color string %s: %s", string, ex)
|
||||
return
|
||||
|
||||
for index, (color, s) in enumerate(parsed):
|
||||
if index + 1 == len(parsed) and pad:
|
||||
# This is the last string so lets append some " " to it
|
||||
s += " " * (self.cols - (col + len(s)) - 1)
|
||||
if trim:
|
||||
dummy, x = screen.getmaxyx()
|
||||
if (col + len(s)) > x:
|
||||
s = "%s..." % s[0:x - 4 - col]
|
||||
screen.addstr(row, col, s, color)
|
||||
col += len(s)
|
||||
return add_string(row, string, screen, self.encoding, **kwargs)
|
||||
|
||||
def draw_statusbars(self):
|
||||
self.add_string(0, self.statusbars.topbar)
|
||||
self.add_string(self.rows - 1, self.statusbars.bottombar)
|
||||
|
||||
# This mode doesn't report errors
|
||||
def report_message(self):
|
||||
pass
|
||||
def draw_statusbars(self, top_row=0, bottom_row=-1, topbar=None, bottombar=None,
|
||||
bottombar_help=True, scr=None):
|
||||
self.add_string(top_row, topbar if topbar else self.statusbars.topbar, scr=scr)
|
||||
bottombar = bottombar if bottombar else self.statusbars.bottombar
|
||||
if bottombar_help:
|
||||
if bottombar_help is True:
|
||||
bottombar_help = self.help_hstr
|
||||
bottombar += " " * (self.cols - len(remove_formatting(bottombar)) -
|
||||
len(remove_formatting(bottombar_help))) + bottombar_help
|
||||
self.add_string(self.rows + bottom_row, bottombar, scr=scr)
|
||||
|
||||
# This mode doesn't do anything with popups
|
||||
def set_popup(self, popup):
|
||||
pass
|
||||
|
||||
# This mode doesn't support marking
|
||||
def clear_marks(self):
|
||||
pass
|
||||
def pause(self):
|
||||
self.paused = True
|
||||
|
||||
def mode_paused(self):
|
||||
return self.paused
|
||||
|
||||
def resume(self):
|
||||
self.paused = False
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
"""
|
||||
@ -199,9 +201,8 @@ class BaseMode(CursesStdIO):
|
||||
# We wrap this function to catch exceptions and shutdown the mainloop
|
||||
try:
|
||||
self.read_input()
|
||||
except Exception as ex:
|
||||
except Exception as ex: # pylint: disable=broad-except
|
||||
log.exception(ex)
|
||||
reactor.stop()
|
||||
|
||||
def read_input(self):
|
||||
# Read the character
|
||||
@ -216,3 +217,121 @@ class BaseMode(CursesStdIO):
|
||||
self.stdscr.keypad(0)
|
||||
curses.echo()
|
||||
curses.endwin()
|
||||
|
||||
|
||||
def add_string(row, string, screen, encoding, col=0, pad=True, pad_char=" ", trim="...", leaveok=0):
|
||||
"""
|
||||
Adds a string to the desired `:param:row`.
|
||||
|
||||
Args:
|
||||
row(int): the row number to write the string
|
||||
row(int): the row number to write the string
|
||||
string(str): the string of text to add
|
||||
scr(curses.window): optional window to add string to instead of self.stdscr
|
||||
col(int): optional starting column offset
|
||||
pad(bool): optional bool if the string should be padded out to the width of the screen
|
||||
trim(bool): optional bool if the string should be trimmed if it is too wide for the screen
|
||||
|
||||
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!"
|
||||
|
||||
Returns:
|
||||
int: the next row
|
||||
|
||||
"""
|
||||
try:
|
||||
parsed = colors.parse_color_string(string, encoding)
|
||||
except colors.BadColorString as ex:
|
||||
log.error("Cannot add bad color string %s: %s", string, ex)
|
||||
return
|
||||
|
||||
if leaveok:
|
||||
screen.leaveok(leaveok)
|
||||
|
||||
max_y, max_x = screen.getmaxyx()
|
||||
for index, (color, s) in enumerate(parsed):
|
||||
if index + 1 == len(parsed) and pad:
|
||||
# This is the last string so lets append some " " to it
|
||||
s += pad_char * (max_x - (col + len(s)))
|
||||
|
||||
# Sometimes the parsed string gives empty elements which may not be printed on max_x
|
||||
if col == max_x:
|
||||
break
|
||||
|
||||
if (col + len(s)) > max_x:
|
||||
if trim:
|
||||
s = "%s%s" % (s[0:max_x - len(trim) - col], trim)
|
||||
else:
|
||||
s = s[0:max_x - col]
|
||||
|
||||
if col + len(s) >= max_x and row == max_y - 1:
|
||||
# Bug in curses when writing to the lower right corner: https://bugs.python.org/issue8243
|
||||
# Use insstr instead which avoids scrolling which is the root cause apparently
|
||||
screen.insstr(row, col, s, color)
|
||||
else:
|
||||
try:
|
||||
screen.addstr(row, col, s, color)
|
||||
except curses.error as ex:
|
||||
import traceback
|
||||
log.warn("FAILED on call screen.addstr(%s, %s, '%s', %s) - max_y: %s, max_x: %s, "
|
||||
"curses.LINES: %s, curses.COLS: %s, Error: '%s', trace:\n%s",
|
||||
row, col, s, color, max_y, max_x, curses.LINES, curses.COLS, ex,
|
||||
"".join(traceback.format_stack(limit=5)))
|
||||
|
||||
col += len(s)
|
||||
|
||||
if leaveok:
|
||||
screen.leaveok(0)
|
||||
|
||||
return row + 1
|
||||
|
||||
|
||||
def mkpanel(color, rows, cols, tly, tlx):
|
||||
win = curses.newwin(rows, cols, tly, tlx)
|
||||
pan = curses.panel.new_panel(win)
|
||||
if curses.has_colors():
|
||||
win.bkgdset(ord(' '), curses.color_pair(color))
|
||||
else:
|
||||
win.bkgdset(ord(' '), curses.A_BOLD)
|
||||
return pan
|
||||
|
||||
|
||||
def mkwin(color, rows, cols, tly, tlx):
|
||||
win = curses.newwin(rows, cols, tly, tlx)
|
||||
if curses.has_colors():
|
||||
win.bkgdset(ord(' '), curses.color_pair(color))
|
||||
else:
|
||||
win.bkgdset(ord(' '), curses.A_BOLD)
|
||||
return win
|
||||
|
||||
|
||||
def mkpad(color, rows, cols):
|
||||
win = curses.newpad(rows, cols)
|
||||
if curses.has_colors():
|
||||
win.bkgdset(ord(' '), curses.color_pair(color))
|
||||
else:
|
||||
win.bkgdset(ord(' '), curses.A_BOLD)
|
||||
return win
|
||||
|
||||
|
||||
def move_cursor(screen, row, col):
|
||||
try:
|
||||
screen.move(row, col)
|
||||
except curses.error as ex:
|
||||
import traceback
|
||||
log.warn("Error on screen.move(%s, %s): (curses.LINES: %s, curses.COLS: %s) Error: '%s'\nStack: %s",
|
||||
row, col, curses.LINES, curses.COLS, ex, "".join(traceback.format_stack()))
|
||||
|
@ -13,22 +13,18 @@ try:
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
import deluge.component as component
|
||||
import deluge.configmanager
|
||||
import deluge.ui.console.colors as colors
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console.commander import Commander
|
||||
from deluge.ui.console.modes import format_utils
|
||||
from deluge.ui.console.modes.basemode import BaseMode
|
||||
|
||||
strwidth = format_utils.strwidth
|
||||
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 curses_util as util
|
||||
from deluge.ui.console.utils import colors
|
||||
from deluge.ui.console.utils.format_utils import delete_alt_backspace, remove_formatting, strwidth
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -51,7 +47,7 @@ def complete_line(line, possible_matches):
|
||||
matches2 = []
|
||||
|
||||
for match in possible_matches:
|
||||
match = format_utils.remove_formatting(match)
|
||||
match = remove_formatting(match)
|
||||
match = match.replace(r"\ ", " ")
|
||||
m1, m2 = "", ""
|
||||
for i, c in enumerate(line):
|
||||
@ -95,11 +91,9 @@ def commonprefix(m):
|
||||
return s2
|
||||
|
||||
|
||||
class Legacy(BaseMode, Commander, component.Component):
|
||||
class CmdLine(BaseMode, Commander):
|
||||
|
||||
def __init__(self, stdscr, encoding=None):
|
||||
|
||||
component.Component.__init__(self, "LegacyUI", 1, depend=["SessionProxy"])
|
||||
|
||||
# Get a handle to the main console
|
||||
self.console = component.get("ConsoleUI")
|
||||
Commander.__init__(self, self.console._commands, interactive=True)
|
||||
@ -112,11 +106,8 @@ class Legacy(BaseMode, Commander, component.Component):
|
||||
self.display_lines_offset = 0
|
||||
|
||||
# Holds the user input and is cleared on 'enter'
|
||||
self.input = ""
|
||||
self.input_incomplete = ""
|
||||
self._old_char = 0
|
||||
self._last_char = 0
|
||||
self._last_del_char = ""
|
||||
self.input = u""
|
||||
self.input_incomplete = u""
|
||||
|
||||
# Keep track of where the cursor is
|
||||
self.input_cursor = 0
|
||||
@ -127,7 +118,7 @@ class Legacy(BaseMode, Commander, component.Component):
|
||||
# Keep track of double- and multi-tabs
|
||||
self.tab_count = 0
|
||||
|
||||
self.console_config = component.get("AllTorrents").config
|
||||
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
|
||||
@ -135,12 +126,11 @@ class Legacy(BaseMode, Commander, component.Component):
|
||||
# that we swap around based on length
|
||||
config_dir = deluge.configmanager.get_config_dir()
|
||||
self.history_file = [
|
||||
os.path.join(config_dir, "legacy.hist1"),
|
||||
os.path.join(config_dir, "legacy.hist2")
|
||||
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["save_legacy_history"]:
|
||||
if self.console_config["cmdline"]["save_command_history"]:
|
||||
try:
|
||||
with open(self.history_file[0], "r") as _file:
|
||||
lines1 = _file.read().splitlines()
|
||||
@ -172,10 +162,10 @@ class Legacy(BaseMode, Commander, component.Component):
|
||||
# if not isinstance(line, unicode):
|
||||
# line = line.encode(self.encoding)
|
||||
# self.lines[i] = line
|
||||
line = format_utils.remove_formatting(line)
|
||||
line = remove_formatting(line)
|
||||
if line.startswith(">>> "):
|
||||
console_input = line[4:]
|
||||
if self.console_config["ignore_duplicate_lines"]:
|
||||
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)
|
||||
@ -184,64 +174,47 @@ class Legacy(BaseMode, Commander, component.Component):
|
||||
|
||||
self.input_history_index = len(self.input_history)
|
||||
|
||||
component.start("LegacyUI")
|
||||
|
||||
# show the cursor
|
||||
curses.curs_set(2)
|
||||
|
||||
BaseMode.__init__(self, stdscr, encoding)
|
||||
|
||||
# This gets fired once we have received the torrents list from the core
|
||||
self.started_deferred = defer.Deferred()
|
||||
|
||||
# Maintain a list of (torrent_id, name) for use in tab completion
|
||||
self.torrents = []
|
||||
|
||||
def on_session_state(result):
|
||||
def on_torrents_status(torrents):
|
||||
for torrent_id, status in torrents.items():
|
||||
self.torrents.append((torrent_id, status["name"]))
|
||||
self.started_deferred.callback(True)
|
||||
|
||||
client.core.get_torrents_status({"id": result}, ["name"]).addCallback(on_torrents_status)
|
||||
client.core.get_session_state().addCallback(on_session_state)
|
||||
|
||||
# Register some event handlers to keep the torrent list up-to-date
|
||||
client.register_event_handler("TorrentAddedEvent", self.on_torrent_added_event)
|
||||
client.register_event_handler("TorrentRemovedEvent", self.on_torrent_removed_event)
|
||||
util.safe_curs_set(util.Curser.VERY_VISIBLE)
|
||||
BaseMode.__init__(self, stdscr, encoding, depend=["SessionProxy"])
|
||||
|
||||
@overrides(component.Component)
|
||||
def update(self):
|
||||
if component.get("ConsoleUI").screen != self:
|
||||
if not component.get("ConsoleUI").is_active_mode(self):
|
||||
return
|
||||
|
||||
# Update just the status bars
|
||||
self.add_string(0, self.statusbars.topbar)
|
||||
self.add_string(self.rows - 2, self.statusbars.bottombar)
|
||||
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()
|
||||
|
||||
# An ugly, ugly, UGLY UGLY way to handle alt+backspace
|
||||
# deleting more characters than it should, but without a more
|
||||
# complex input handling system, a more elegant solution
|
||||
# is not viable
|
||||
if self._old_char == 27 and self._last_char == 127:
|
||||
self.input += self._last_del_char
|
||||
self.input_cursor += 1
|
||||
self._old_char = self._last_char
|
||||
self._last_char = c
|
||||
# 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 != 9:
|
||||
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 == curses.KEY_ENTER or c == 10:
|
||||
if c in [curses.KEY_ENTER, util.KEY_ENTER2]:
|
||||
if self.input:
|
||||
self.input = self.input.encode(self.encoding)
|
||||
|
||||
if self.input.endswith("\\"):
|
||||
self.input = self.input[:-1]
|
||||
self.input_cursor -= 1
|
||||
@ -251,7 +224,7 @@ class Legacy(BaseMode, Commander, component.Component):
|
||||
# Remove the oldest input history if the max history size
|
||||
# is reached.
|
||||
del self.input_history[0]
|
||||
if self.console_config["ignore_duplicate_lines"]:
|
||||
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)
|
||||
@ -260,13 +233,13 @@ class Legacy(BaseMode, Commander, component.Component):
|
||||
else:
|
||||
self.input_history.append(self.input)
|
||||
self.input_history_index = len(self.input_history)
|
||||
self.input = ""
|
||||
self.input_incomplete = ""
|
||||
self.input = u""
|
||||
self.input_incomplete = u""
|
||||
self.input_cursor = 0
|
||||
self.stdscr.refresh()
|
||||
|
||||
# Run the tab completer function
|
||||
elif c == 9:
|
||||
elif c == util.KEY_TAB:
|
||||
# Keep track of tab hit count to know when it's double-hit
|
||||
self.tab_count += 1
|
||||
|
||||
@ -326,31 +299,13 @@ class Legacy(BaseMode, Commander, component.Component):
|
||||
self.refresh()
|
||||
|
||||
# Delete a character in the input string based on cursor position
|
||||
elif c == curses.KEY_BACKSPACE or c == 127:
|
||||
elif c in [curses.KEY_BACKSPACE, util.KEY_BACKSPACE2]:
|
||||
if self.input and self.input_cursor > 0:
|
||||
self._last_del_char = self.input[self.input_cursor - 1]
|
||||
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 == 27:
|
||||
sep_chars = " *?!._~-#$^;'\"/"
|
||||
deleted = 0
|
||||
seg_start = self.input[:self.input_cursor]
|
||||
seg_end = self.input[self.input_cursor:]
|
||||
|
||||
while seg_start and self.input_cursor > 0:
|
||||
if (not seg_start) or (self.input_cursor == 0):
|
||||
break
|
||||
if deleted and seg_start[-1] in sep_chars:
|
||||
break
|
||||
|
||||
seg_start = seg_start[:-1]
|
||||
deleted += 1
|
||||
self.input_cursor -= 1
|
||||
|
||||
self.input = seg_start + seg_end
|
||||
|
||||
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:]
|
||||
@ -378,30 +333,23 @@ class Legacy(BaseMode, Commander, component.Component):
|
||||
# Move the cursor forward
|
||||
self.input_cursor += 1
|
||||
|
||||
# Update the input string on the screen
|
||||
self.add_string(self.rows - 1, self.input)
|
||||
try:
|
||||
self.stdscr.move(self.rows - 1, self.input_cursor)
|
||||
except curses.error:
|
||||
pass
|
||||
self.stdscr.refresh()
|
||||
self.refresh()
|
||||
|
||||
def on_resize(self, *args):
|
||||
BaseMode.on_resize_norefresh(self, *args)
|
||||
|
||||
# We need to also refresh AllTorrents because otherwise it will
|
||||
# be only us that get properly resized
|
||||
all_torrents = component.get("AllTorrents")
|
||||
all_torrents.on_resize(*args)
|
||||
@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
|
||||
@ -427,16 +375,9 @@ class Legacy(BaseMode, Commander, component.Component):
|
||||
self.add_string(index + 1, line)
|
||||
|
||||
# Add the input string
|
||||
self.add_string(self.rows - 1, self.input)
|
||||
self.add_string(self.rows - 1, self.input, pad=False, trim=False)
|
||||
|
||||
if component.get("ConsoleUI").screen != self:
|
||||
return
|
||||
|
||||
# Move the cursor
|
||||
try:
|
||||
self.stdscr.move(self.rows - 1, self.input_cursor)
|
||||
except curses.error:
|
||||
pass
|
||||
move_cursor(self.stdscr, self.rows - 1, min(self.input_cursor, curses.COLS - 1))
|
||||
self.stdscr.redrawwin()
|
||||
self.stdscr.refresh()
|
||||
|
||||
@ -467,7 +408,7 @@ class Legacy(BaseMode, Commander, component.Component):
|
||||
|
||||
"""
|
||||
|
||||
if self.console_config["save_legacy_history"]:
|
||||
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:
|
||||
@ -563,7 +504,7 @@ class Legacy(BaseMode, Commander, component.Component):
|
||||
if refresh:
|
||||
self.refresh()
|
||||
|
||||
def add_string(self, row, string):
|
||||
def _add_string(self, row, string):
|
||||
"""
|
||||
Adds a string to the desired `:param:row`.
|
||||
|
||||
@ -663,7 +604,7 @@ class Legacy(BaseMode, Commander, component.Component):
|
||||
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 = format_utils.remove_formatting(new_line)
|
||||
new_line = remove_formatting(new_line)
|
||||
return (new_line, len(new_line))
|
||||
else:
|
||||
if hits == 1:
|
||||
@ -676,11 +617,11 @@ class Legacy(BaseMode, Commander, component.Component):
|
||||
|
||||
new_line = " ".join([p, complete_line(l_arg, possible_matches)]).lstrip()
|
||||
|
||||
if len(format_utils.remove_formatting(new_line)) > len(line):
|
||||
if len(remove_formatting(new_line)) > len(line):
|
||||
line = new_line
|
||||
cursor = len(line)
|
||||
elif hits >= 2:
|
||||
max_list = self.console_config["torrents_per_tab_press"]
|
||||
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
|
||||
@ -691,7 +632,7 @@ class Legacy(BaseMode, Commander, component.Component):
|
||||
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["third_tab_lists_all"]:
|
||||
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]
|
||||
@ -716,7 +657,7 @@ class Legacy(BaseMode, Commander, component.Component):
|
||||
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 = format_utils.remove_formatting(line)
|
||||
line = remove_formatting(line)
|
||||
cursor = len(line)
|
||||
return (line, cursor)
|
||||
|
||||
@ -819,7 +760,7 @@ class Legacy(BaseMode, Commander, component.Component):
|
||||
|
||||
match_count = 0
|
||||
match_count2 = 0
|
||||
for torrent_id, torrent_name in self.torrents:
|
||||
for torrent_id, torrent_name in self.console.torrents:
|
||||
if torrent_id.startswith(line):
|
||||
match_count += 1
|
||||
if torrent_name.startswith(line):
|
||||
@ -828,7 +769,7 @@ class Legacy(BaseMode, Commander, component.Component):
|
||||
match_count2 += 1
|
||||
|
||||
# Find all possible matches
|
||||
for torrent_id, torrent_name in self.torrents:
|
||||
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(" ", "\\ ")
|
||||
@ -862,52 +803,3 @@ class Legacy(BaseMode, Commander, component.Component):
|
||||
possible_matches2.append(text)
|
||||
|
||||
return possible_matches + possible_matches2
|
||||
|
||||
def get_torrent_name(self, torrent_id):
|
||||
"""
|
||||
Gets a torrent name from the torrents list.
|
||||
|
||||
:param torrent_id: str, the torrent_id
|
||||
|
||||
:returns: the name of the torrent or None
|
||||
"""
|
||||
|
||||
for tid, name in self.torrents:
|
||||
if torrent_id == tid:
|
||||
return name
|
||||
|
||||
return None
|
||||
|
||||
def match_torrent(self, string):
|
||||
"""
|
||||
Returns a list of torrent_id matches for the string. It will search both
|
||||
torrent_ids and torrent names, but will only return torrent_ids.
|
||||
|
||||
:param string: str, the string to match on
|
||||
|
||||
:returns: list of matching torrent_ids. Will return an empty list if
|
||||
no matches are found.
|
||||
|
||||
"""
|
||||
|
||||
if not isinstance(string, unicode):
|
||||
string = unicode(string, self.encoding)
|
||||
|
||||
ret = []
|
||||
for tid, name in self.torrents:
|
||||
if not isinstance(name, unicode):
|
||||
name = unicode(name, self.encoding)
|
||||
if tid.startswith(string) or name.startswith(string):
|
||||
ret.append(tid)
|
||||
|
||||
return ret
|
||||
|
||||
def on_torrent_added_event(self, event, from_state=False):
|
||||
def on_torrent_status(status):
|
||||
self.torrents.append((event, status["name"]))
|
||||
client.core.get_torrent_status(event, ["name"]).addCallback(on_torrent_status)
|
||||
|
||||
def on_torrent_removed_event(self, event):
|
||||
for index, (tid, name) in enumerate(self.torrents):
|
||||
if event == tid:
|
||||
del self.torrents[index]
|
@ -1,78 +0,0 @@
|
||||
# -*- 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.
|
||||
#
|
||||
|
||||
import logging
|
||||
|
||||
import deluge.common
|
||||
from deluge.ui.console.modes import format_utils
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def format_queue(qnum):
|
||||
if qnum >= 0:
|
||||
return "%d" % (qnum + 1)
|
||||
else:
|
||||
return ""
|
||||
|
||||
columns = {
|
||||
"#": (("queue",), format_queue),
|
||||
"Name": (("name",), None),
|
||||
"Size": (("total_wanted",), deluge.common.fsize),
|
||||
"State": (("state",), None),
|
||||
"Progress": (("progress",), format_utils.format_progress),
|
||||
"Seeds": (("num_seeds", "total_seeds"), format_utils.format_seeds_peers),
|
||||
"Peers": (("num_peers", "total_peers"), format_utils.format_seeds_peers),
|
||||
"Down Speed": (("download_payload_rate",), format_utils.format_speed),
|
||||
"Up Speed": (("upload_payload_rate",), format_utils.format_speed),
|
||||
"ETA": (("eta",), format_utils.format_time),
|
||||
"Ratio": (("ratio",), format_utils.format_float),
|
||||
"Avail": (("distributed_copies",), format_utils.format_float),
|
||||
"Added": (("time_added",), deluge.common.fdate),
|
||||
"Tracker": (("tracker_host",), None),
|
||||
"Download Folder": (("download_location",), None),
|
||||
"Downloaded": (("all_time_download",), deluge.common.fsize),
|
||||
"Uploaded": (("total_uploaded",), deluge.common.fsize),
|
||||
"Remaining": (("total_remaining",), deluge.common.fsize),
|
||||
"Owner": (("owner",), None),
|
||||
"Shared": (("shared",), str),
|
||||
"Active Time": (("active_time",), deluge.common.ftime),
|
||||
"Seeding Time": (("seeding_time",), deluge.common.ftime),
|
||||
"Complete Seen": (("last_seen_complete",), format_utils.format_date_never),
|
||||
"Completed": (("completed_time",), format_utils.format_date),
|
||||
"Seeds:Peers": (("seeds_peers_ratio",), format_utils.format_float),
|
||||
"Down Limit": (("max_download_speed",), format_utils.format_speed),
|
||||
"Up Limit": (("max_upload_speed",), format_utils.format_speed),
|
||||
}
|
||||
|
||||
|
||||
def get_column_value(name, state):
|
||||
try:
|
||||
col = columns[name]
|
||||
except KeyError:
|
||||
return "Please Wait"
|
||||
|
||||
if col[1]:
|
||||
try:
|
||||
args = [state[key] for key in col[0]]
|
||||
except KeyError:
|
||||
return "Please Wait"
|
||||
return col[1](*args)
|
||||
else:
|
||||
try:
|
||||
return state[col[0][0]]
|
||||
except KeyError:
|
||||
return "Please Wait"
|
||||
|
||||
|
||||
def get_required_fields(cols):
|
||||
fields = []
|
||||
for col in cols:
|
||||
fields.extend(columns.get(col)[0])
|
||||
return fields
|
@ -7,21 +7,17 @@
|
||||
# See LICENSE for more details.
|
||||
#
|
||||
|
||||
""" A mode that show's a popup to select which host to connect to """
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import time
|
||||
from collections import deque
|
||||
|
||||
import deluge.component as component
|
||||
from deluge.configmanager import ConfigManager
|
||||
from deluge.decorators import overrides
|
||||
from deluge.ui import common as uicommon
|
||||
from deluge.ui.client import Client, client
|
||||
from deluge.ui.console.modes.alltorrents import AllTorrents
|
||||
from deluge.ui.console.modes.basemode import BaseMode
|
||||
from deluge.ui.console.modes.input_popup import InputPopup
|
||||
from deluge.ui.console.modes.popup import MessagePopup, SelectablePopup
|
||||
from deluge.ui.console.widgets.popup import InputPopup, PopupsHandler, SelectablePopup
|
||||
|
||||
try:
|
||||
import curses
|
||||
@ -31,36 +27,48 @@ except ImportError:
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConnectionManager(BaseMode):
|
||||
class ConnectionManager(BaseMode, PopupsHandler):
|
||||
|
||||
def __init__(self, stdscr, encoding=None):
|
||||
self.popup = None
|
||||
PopupsHandler.__init__(self)
|
||||
self.statuses = {}
|
||||
self.messages = deque()
|
||||
self.all_torrents = None
|
||||
self.config = ConfigManager("hostlist.conf.1.2", uicommon.DEFAULT_HOSTS)
|
||||
BaseMode.__init__(self, stdscr, encoding)
|
||||
self.__update_statuses()
|
||||
self.__update_popup()
|
||||
self.update_hosts_status()
|
||||
BaseMode.__init__(self, stdscr, encoding=encoding)
|
||||
self.update_select_host_popup()
|
||||
|
||||
def update_select_host_popup(self):
|
||||
selected_index = None
|
||||
if self.popup:
|
||||
selected_index = self.popup.current_selection()
|
||||
|
||||
popup = SelectablePopup(self, _("Select Host"), self._host_selected, border_off_west=1, active_wrap=True)
|
||||
popup.add_header("{!white,black,bold!}'Q'=%s, 'a'=%s, 'D'=%s" %
|
||||
(_("quit"), _("add new host"), _("delete host")),
|
||||
space_below=True)
|
||||
self.push_popup(popup, clear=True)
|
||||
|
||||
def __update_popup(self):
|
||||
self.popup = SelectablePopup(self, "Select Host", self.__host_selected)
|
||||
self.popup.add_line("{!white,black,bold!}'Q'=quit, 'r'=refresh, 'a'=add new host, 'D'=delete host",
|
||||
selectable=False)
|
||||
for host in self.config["hosts"]:
|
||||
args = {"data": host[0], "foreground": "red"}
|
||||
state = "Offline"
|
||||
if host[0] in self.statuses:
|
||||
self.popup.add_line("%s:%d [Online] (%s)" % (host[1], host[2], self.statuses[host[0]]),
|
||||
data=host[0], foreground="green")
|
||||
else:
|
||||
self.popup.add_line("%s:%d [Offline]" % (host[1], host[2]), data=host[0], foreground="red")
|
||||
state = "Online"
|
||||
args.update({"data": self.statuses[host[0]], "foreground": "green"})
|
||||
host_str = "%s:%d [%s]" % (host[1], host[2], state)
|
||||
self.popup.add_line(host[0], host_str, selectable=True, use_underline=True, **args)
|
||||
|
||||
if selected_index is not None:
|
||||
self.popup.set_selection(selected_index)
|
||||
self.inlist = True
|
||||
self.refresh()
|
||||
|
||||
def __update_statuses(self):
|
||||
def update_hosts_status(self):
|
||||
"""Updates the host status"""
|
||||
def on_connect(result, c, host_id):
|
||||
def on_info(info, c):
|
||||
self.statuses[host_id] = info
|
||||
self.__update_popup()
|
||||
self.update_select_host_popup()
|
||||
c.disconnect()
|
||||
|
||||
def on_info_fail(reason, c):
|
||||
@ -75,6 +83,7 @@ class ConnectionManager(BaseMode):
|
||||
def on_connect_failed(reason, host_id):
|
||||
if host_id in self.statuses:
|
||||
del self.statuses[host_id]
|
||||
self.update_select_host_popup()
|
||||
|
||||
for host in self.config["hosts"]:
|
||||
c = Client()
|
||||
@ -87,28 +96,42 @@ class ConnectionManager(BaseMode):
|
||||
d.addCallback(on_connect, c, host[0])
|
||||
d.addErrback(on_connect_failed, host[0])
|
||||
|
||||
def __on_connected(self, result):
|
||||
component.start()
|
||||
self.stdscr.erase()
|
||||
at = AllTorrents(self.stdscr, self.encoding)
|
||||
component.get("ConsoleUI").set_mode(at)
|
||||
at.resume()
|
||||
def _on_connected(self, result):
|
||||
d = component.get("ConsoleUI").start_console()
|
||||
|
||||
def __host_selected(self, idx, data):
|
||||
def on_console_start(result):
|
||||
component.get("ConsoleUI").set_mode("TorrentList")
|
||||
d.addCallback(on_console_start)
|
||||
|
||||
def _on_connect_fail(self, result):
|
||||
self.report_message("Failed to connect!", result)
|
||||
self.refresh()
|
||||
if hasattr(result, "getTraceback"):
|
||||
log.exception(result)
|
||||
|
||||
def _host_selected(self, selected_host, *args, **kwargs):
|
||||
if selected_host not in self.statuses:
|
||||
return
|
||||
for host in self.config["hosts"]:
|
||||
if host[0] == data and host[0] in self.statuses:
|
||||
client.connect(host[1], host[2], host[3], host[4]).addCallback(self.__on_connected)
|
||||
if host[0] == selected_host:
|
||||
d = client.connect(host[1], host[2], host[3], host[4])
|
||||
d.addCallback(self._on_connected)
|
||||
d.addErrback(self._on_connect_fail)
|
||||
return
|
||||
return False
|
||||
|
||||
def __do_add(self, result):
|
||||
hostname = result["hostname"]
|
||||
def _do_add(self, result, **kwargs):
|
||||
if not result or kwargs.get("close", False):
|
||||
self.pop_popup()
|
||||
return
|
||||
hostname = result["hostname"]["value"]
|
||||
try:
|
||||
port = int(result["port"])
|
||||
port = int(result["port"]["value"])
|
||||
except ValueError:
|
||||
self.report_message("Can't add host", "Invalid port. Must be an integer")
|
||||
return
|
||||
username = result["username"]
|
||||
password = result["password"]
|
||||
username = result["username"]["value"]
|
||||
password = result["password"]["value"]
|
||||
for host in self.config["hosts"]:
|
||||
if (host[1], host[2], host[3]) == (hostname, port, username):
|
||||
self.report_message("Can't add host", "Host already in list")
|
||||
@ -116,18 +139,21 @@ class ConnectionManager(BaseMode):
|
||||
newid = hashlib.sha1(str(time.time())).hexdigest()
|
||||
self.config["hosts"].append((newid, hostname, port, username, password))
|
||||
self.config.save()
|
||||
self.__update_popup()
|
||||
self.update_select_host_popup()
|
||||
|
||||
def __add_popup(self):
|
||||
def add_popup(self):
|
||||
self.inlist = False
|
||||
self.popup = InputPopup(self, "Add Host (up & down arrows to navigate, esc to cancel)", close_cb=self.__do_add)
|
||||
self.popup.add_text_input("Hostname:", "hostname")
|
||||
self.popup.add_text_input("Port:", "port")
|
||||
self.popup.add_text_input("Username:", "username")
|
||||
self.popup.add_text_input("Password:", "password")
|
||||
popup = InputPopup(self, "Add Host (up & down arrows to navigate, esc to cancel)",
|
||||
border_off_north=1, border_off_east=1,
|
||||
close_cb=self._do_add)
|
||||
popup.add_text_input("hostname", "%s:" % _("Hostname"))
|
||||
popup.add_text_input("port", "%s:" % _("Port"))
|
||||
popup.add_text_input("username", "%s:" % _("Username"))
|
||||
popup.add_text_input("password", "%s:" % _("Password"))
|
||||
self.push_popup(popup, clear=True)
|
||||
self.refresh()
|
||||
|
||||
def __delete_current_host(self):
|
||||
def delete_current_host(self):
|
||||
idx, data = self.popup.current_selection()
|
||||
log.debug("deleting host: %s", data)
|
||||
for host in self.config["hosts"]:
|
||||
@ -136,26 +162,42 @@ class ConnectionManager(BaseMode):
|
||||
break
|
||||
self.config.save()
|
||||
|
||||
def report_message(self, title, message):
|
||||
self.messages.append((title, message))
|
||||
@overrides(component.Component)
|
||||
def start(self):
|
||||
self.refresh()
|
||||
|
||||
@overrides(component.Component)
|
||||
def update(self):
|
||||
self.update_hosts_status()
|
||||
|
||||
@overrides(BaseMode)
|
||||
def pause(self):
|
||||
self.pop_popup()
|
||||
BaseMode.pause(self)
|
||||
|
||||
@overrides(BaseMode)
|
||||
def resume(self):
|
||||
BaseMode.resume(self)
|
||||
self.refresh()
|
||||
|
||||
@overrides(BaseMode)
|
||||
def refresh(self):
|
||||
if self.mode_paused():
|
||||
return
|
||||
|
||||
self.stdscr.erase()
|
||||
self.draw_statusbars()
|
||||
self.stdscr.noutrefresh()
|
||||
|
||||
if self.popup is None and self.messages:
|
||||
title, msg = self.messages.popleft()
|
||||
self.popup = MessagePopup(self, title, msg)
|
||||
|
||||
if not self.popup:
|
||||
self.__update_popup()
|
||||
self.update_select_host_popup()
|
||||
|
||||
self.popup.refresh()
|
||||
curses.doupdate()
|
||||
|
||||
def on_resize(self, *args):
|
||||
BaseMode.on_resize_norefresh(self, *args)
|
||||
@overrides(BaseMode)
|
||||
def on_resize(self, rows, cols):
|
||||
BaseMode.on_resize(self, rows, cols)
|
||||
|
||||
if self.popup:
|
||||
self.popup.handle_resize()
|
||||
@ -163,8 +205,8 @@ class ConnectionManager(BaseMode):
|
||||
self.stdscr.erase()
|
||||
self.refresh()
|
||||
|
||||
@overrides(BaseMode)
|
||||
def read_input(self):
|
||||
# Read the character
|
||||
c = self.stdscr.getch()
|
||||
|
||||
if c > 31 and c < 256:
|
||||
@ -180,17 +222,15 @@ class ConnectionManager(BaseMode):
|
||||
reactor.stop()
|
||||
return
|
||||
if chr(c) == "D" and self.inlist:
|
||||
self.__delete_current_host()
|
||||
self.__update_popup()
|
||||
self.delete_current_host()
|
||||
self.update_select_host_popup()
|
||||
return
|
||||
if chr(c) == "r" and self.inlist:
|
||||
self.__update_statuses()
|
||||
if chr(c) == "a" and self.inlist:
|
||||
self.__add_popup()
|
||||
self.add_popup()
|
||||
return
|
||||
|
||||
if self.popup:
|
||||
if self.popup.handle_read(c):
|
||||
self.popup = None
|
||||
if self.popup.handle_read(c) and self.popup.closed():
|
||||
self.pop_popup()
|
||||
self.refresh()
|
||||
return
|
||||
|
@ -10,9 +10,9 @@
|
||||
import logging
|
||||
|
||||
import deluge.component as component
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console.modes import format_utils
|
||||
from deluge.decorators import overrides
|
||||
from deluge.ui.console.modes.basemode import BaseMode
|
||||
from deluge.ui.console.utils import curses_util as util
|
||||
|
||||
try:
|
||||
import curses
|
||||
@ -23,30 +23,28 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EventView(BaseMode):
|
||||
|
||||
def __init__(self, parent_mode, stdscr, encoding=None):
|
||||
BaseMode.__init__(self, stdscr, encoding)
|
||||
self.parent_mode = parent_mode
|
||||
self.offset = 0
|
||||
BaseMode.__init__(self, stdscr, encoding)
|
||||
|
||||
def back_to_overview(self):
|
||||
component.get("ConsoleUI").set_mode(self.parent_mode.mode_name)
|
||||
|
||||
@overrides(component.Component)
|
||||
def update(self):
|
||||
self.refresh()
|
||||
|
||||
@overrides(BaseMode)
|
||||
def refresh(self):
|
||||
"This method just shows each line of the event log"
|
||||
"""
|
||||
This method just shows each line of the event log
|
||||
"""
|
||||
events = component.get("ConsoleUI").events
|
||||
|
||||
self.stdscr.erase()
|
||||
|
||||
self.add_string(0, self.statusbars.topbar)
|
||||
hstr = "%sPress [h] for help" % (" " * (self.cols - len(self.statusbars.bottombar) - 10))
|
||||
# This will quite likely fail when switching modes
|
||||
try:
|
||||
rf = format_utils.remove_formatting
|
||||
string = self.statusbars.bottombar
|
||||
hstr = "Press {!magenta,blue,bold!}[h]{!status!} for help"
|
||||
|
||||
string += " " * (self.cols - len(rf(string)) - len(rf(hstr))) + hstr
|
||||
|
||||
self.add_string(self.rows - 1, string)
|
||||
except Exception as ex:
|
||||
log.debug("Exception caught: %s", ex)
|
||||
self.draw_statusbars()
|
||||
|
||||
if events:
|
||||
for i, event in enumerate(events):
|
||||
@ -65,44 +63,22 @@ class EventView(BaseMode):
|
||||
else:
|
||||
self.add_string(1, "{!white,black,bold!}No events to show yet")
|
||||
|
||||
if component.get("ConsoleUI").screen != self:
|
||||
if not component.get("ConsoleUI").is_active_mode(self):
|
||||
return
|
||||
|
||||
self.stdscr.noutrefresh()
|
||||
curses.doupdate()
|
||||
|
||||
def on_resize(self, *args):
|
||||
BaseMode.on_resize_norefresh(self, *args)
|
||||
|
||||
# Always refresh Legacy(it will also refresh AllTorrents), otherwise it will bug deluge out
|
||||
legacy = component.get("LegacyUI")
|
||||
legacy.on_resize(*args)
|
||||
self.stdscr.erase()
|
||||
@overrides(BaseMode)
|
||||
def on_resize(self, rows, cols):
|
||||
BaseMode.on_resize(self, rows, cols)
|
||||
self.refresh()
|
||||
|
||||
def back_to_overview(self):
|
||||
self.stdscr.erase()
|
||||
component.get("ConsoleUI").set_mode(self.parent_mode)
|
||||
self.parent_mode.resume()
|
||||
|
||||
@overrides(BaseMode)
|
||||
def read_input(self):
|
||||
c = self.stdscr.getch()
|
||||
|
||||
if c > 31 and c < 256:
|
||||
if chr(c) == "Q":
|
||||
from twisted.internet import reactor
|
||||
if client.connected():
|
||||
def on_disconnect(result):
|
||||
reactor.stop()
|
||||
client.disconnect().addCallback(on_disconnect)
|
||||
else:
|
||||
reactor.stop()
|
||||
return
|
||||
elif chr(c) == "q":
|
||||
self.back_to_overview()
|
||||
return
|
||||
|
||||
if c == 27:
|
||||
if c in [ord('q'), util.KEY_ESC]:
|
||||
self.back_to_overview()
|
||||
return
|
||||
|
||||
|
@ -1,916 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||
# 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.
|
||||
#
|
||||
|
||||
from __future__ import division
|
||||
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
|
||||
from deluge.ui.console import colors
|
||||
from deluge.ui.console.modes.popup import ALIGN, Popup
|
||||
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InputField(object):
|
||||
depend = None
|
||||
# render the input. return number of rows taken up
|
||||
|
||||
def get_height(self):
|
||||
return 0
|
||||
|
||||
def render(self, screen, row, width, selected, col=1):
|
||||
return 0
|
||||
|
||||
def handle_read(self, c):
|
||||
if c in [curses.KEY_ENTER, 10, 127, 113]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_value(self):
|
||||
return None
|
||||
|
||||
def set_value(self, value):
|
||||
pass
|
||||
|
||||
def set_depend(self, i, inverse=False):
|
||||
if not isinstance(i, CheckedInput):
|
||||
raise Exception("Can only depend on CheckedInputs")
|
||||
self.depend = i
|
||||
self.inverse = inverse
|
||||
|
||||
def depend_skip(self):
|
||||
if not self.depend:
|
||||
return False
|
||||
if self.inverse:
|
||||
return self.depend.checked
|
||||
else:
|
||||
return not self.depend.checked
|
||||
|
||||
|
||||
class CheckedInput(InputField):
|
||||
def __init__(self, parent, message, name, checked=False, additional_formatting=False):
|
||||
self.parent = parent
|
||||
self.additional_formatting = additional_formatting
|
||||
self.chkd_inact = "[X] %s" % message
|
||||
self.unchkd_inact = "[ ] %s" % message
|
||||
self.chkd_act = "[{!black,white,bold!}X{!white,black!}] %s" % message
|
||||
self.unchkd_act = "[{!black,white,bold!} {!white,black!}] %s" % message
|
||||
self.name = name
|
||||
self.checked = checked
|
||||
|
||||
def get_height(self):
|
||||
return 1
|
||||
|
||||
def render(self, screen, row, width, active, col=1):
|
||||
if self.checked and active:
|
||||
self.parent.add_string(row, self.chkd_act, screen, col, False, True)
|
||||
elif self.checked:
|
||||
self.parent.add_string(row, self.chkd_inact, screen, col, False, True)
|
||||
elif active:
|
||||
self.parent.add_string(row, self.unchkd_act, screen, col, False, True)
|
||||
else:
|
||||
self.parent.add_string(row, self.unchkd_inact, screen, col, False, True)
|
||||
return 1
|
||||
|
||||
def handle_read(self, c):
|
||||
if c == 32:
|
||||
self.checked = not self.checked
|
||||
|
||||
def get_value(self):
|
||||
return self.checked
|
||||
|
||||
def set_value(self, c):
|
||||
self.checked = c
|
||||
|
||||
|
||||
class CheckedPlusInput(InputField):
|
||||
def __init__(self, parent, message, name, child, checked=False, additional_formatting=False):
|
||||
self.parent = parent
|
||||
self.additional_formatting = additional_formatting
|
||||
self.chkd_inact = "[X] %s" % message
|
||||
self.unchkd_inact = "[ ] %s" % message
|
||||
self.chkd_act = "[{!black,white,bold!}X{!white,black!}] %s" % message
|
||||
self.unchkd_act = "[{!black,white,bold!} {!white,black!}] %s" % message
|
||||
self.name = name
|
||||
self.checked = checked
|
||||
self.msglen = len(self.chkd_inact) + 1
|
||||
self.child = child
|
||||
self.child_active = False
|
||||
|
||||
def get_height(self):
|
||||
return max(2, self.child.height)
|
||||
|
||||
def render(self, screen, row, width, active, col=1):
|
||||
isact = active and not self.child_active
|
||||
if self.checked and isact:
|
||||
self.parent.add_string(row, self.chkd_act, screen, col, False, True)
|
||||
elif self.checked:
|
||||
self.parent.add_string(row, self.chkd_inact, screen, col, False, True)
|
||||
elif isact:
|
||||
self.parent.add_string(row, self.unchkd_act, screen, col, False, True)
|
||||
else:
|
||||
self.parent.add_string(row, self.unchkd_inact, screen, col, False, True)
|
||||
|
||||
if active and self.checked and self.child_active:
|
||||
self.parent.add_string(row + 1, "(esc to leave)", screen, col, False, True)
|
||||
elif active and self.checked:
|
||||
self.parent.add_string(row + 1, "(right arrow to edit)", screen, col, False, True)
|
||||
rows = 2
|
||||
# show child
|
||||
if self.checked:
|
||||
if isinstance(self.child, (TextInput, IntSpinInput, FloatSpinInput)):
|
||||
crows = self.child.render(screen, row, width - self.msglen,
|
||||
self.child_active and active, col + self.msglen, self.msglen)
|
||||
else:
|
||||
crows = self.child.render(screen, row, width - self.msglen,
|
||||
self.child_active and active, col + self.msglen)
|
||||
rows = max(rows, crows)
|
||||
else:
|
||||
self.parent.add_string(row, "(enable to view/edit value)", screen, col + self.msglen, False, True)
|
||||
return rows
|
||||
|
||||
def handle_read(self, c):
|
||||
if self.child_active:
|
||||
if c == 27: # leave child on esc
|
||||
self.child_active = False
|
||||
return
|
||||
# pass keys through to child
|
||||
self.child.handle_read(c)
|
||||
else:
|
||||
if c == 32:
|
||||
self.checked = not self.checked
|
||||
if c == curses.KEY_RIGHT:
|
||||
self.child_active = True
|
||||
|
||||
def get_value(self):
|
||||
return self.checked
|
||||
|
||||
def set_value(self, c):
|
||||
self.checked = c
|
||||
|
||||
def get_child(self):
|
||||
return self.child
|
||||
|
||||
|
||||
class IntSpinInput(InputField):
|
||||
def __init__(self, parent, message, name, move_func, value, min_val=None, max_val=None,
|
||||
additional_formatting=False):
|
||||
self.parent = parent
|
||||
self.message = message
|
||||
self.name = name
|
||||
|
||||
self.additional_formatting = additional_formatting
|
||||
|
||||
self.default_str = str(value)
|
||||
self.set_value(value)
|
||||
self.default_value = self.value
|
||||
|
||||
self.cursor = len(self.valstr)
|
||||
self.cursoff = colors.get_line_width(self.message) + 4 # + 4 for the " [ " in the rendered string
|
||||
self.move_func = move_func
|
||||
self.min_val = min_val
|
||||
self.max_val = max_val
|
||||
self.need_update = False
|
||||
|
||||
def get_height(self):
|
||||
return 1
|
||||
|
||||
def __limit_value(self):
|
||||
if (self.min_val is not None) and self.value < self.min_val:
|
||||
self.value = self.min_val
|
||||
if (self.max_val is not None) and self.value > self.max_val:
|
||||
self.value = self.max_val
|
||||
|
||||
def render(self, screen, row, width, active, col=1, cursor_offset=0):
|
||||
if not active and self.need_update:
|
||||
if not self.valstr or self.valstr == "-":
|
||||
self.value = self.default_value
|
||||
self.valstr = self.default_str
|
||||
try:
|
||||
int(self.value)
|
||||
except ValueError:
|
||||
self.real_value = False
|
||||
else:
|
||||
self.value = int(self.valstr)
|
||||
self.__limit_value()
|
||||
self.valstr = "%d" % self.value
|
||||
self.cursor = len(self.valstr)
|
||||
self.cursor = colors.get_line_width(self.valstr)
|
||||
self.need_update = False
|
||||
elif self.need_update and self.valstr != "-":
|
||||
self.real_value = True
|
||||
try:
|
||||
self.value = int(self.valstr)
|
||||
except ValueError:
|
||||
self.value = self.default_value
|
||||
try:
|
||||
int(self.value)
|
||||
except ValueError:
|
||||
self.real_value = False
|
||||
if not self.valstr:
|
||||
self.parent.add_string(row, "%s {!input!}[ ]" % self.message, screen, col, False, True)
|
||||
elif active:
|
||||
self.parent.add_string(row, "%s {!input!}[ {!black,white,bold!}%s{!input!} ]" % (
|
||||
self.message, self.valstr), screen, col, False, True)
|
||||
elif self.additional_formatting and self.valstr == self.default_str:
|
||||
self.parent.add_string(row, "%s {!input!}[ {!magenta,black!}%s{!input!} ]" % (
|
||||
self.message, self.valstr), screen, col, False, True)
|
||||
else:
|
||||
self.parent.add_string(row, "%s {!input!}[ %s ]" % (self.message, self.valstr), screen, col, False, True)
|
||||
|
||||
if active:
|
||||
self.move_func(row, self.cursor + self.cursoff + cursor_offset)
|
||||
|
||||
return 1
|
||||
|
||||
def handle_read(self, c):
|
||||
if c == curses.KEY_PPAGE and self.value < self.max_val:
|
||||
if not self.real_value:
|
||||
self.value = self.min_val
|
||||
self.valstr = "%d" % self.value
|
||||
self.real_value = True
|
||||
else:
|
||||
self.value += 1
|
||||
self.valstr = "%d" % self.value
|
||||
self.cursor = len(self.valstr)
|
||||
elif c == curses.KEY_NPAGE and self.value > self.min_val:
|
||||
if not self.real_value:
|
||||
self.value = self.min_val
|
||||
self.valstr = "%d" % self.value
|
||||
self.real_value = True
|
||||
else:
|
||||
self.value -= 1
|
||||
self.valstr = "%d" % self.value
|
||||
self.cursor = len(self.valstr)
|
||||
elif c == curses.KEY_LEFT:
|
||||
if not self.real_value:
|
||||
return None
|
||||
self.cursor = max(0, self.cursor - 1)
|
||||
elif c == curses.KEY_RIGHT:
|
||||
if not self.real_value:
|
||||
return None
|
||||
self.cursor = min(len(self.valstr), self.cursor + 1)
|
||||
elif c == curses.KEY_HOME:
|
||||
if not self.real_value:
|
||||
return None
|
||||
self.cursor = 0
|
||||
elif c == curses.KEY_END:
|
||||
if not self.real_value:
|
||||
return None
|
||||
self.cursor = len(self.valstr)
|
||||
elif c == curses.KEY_BACKSPACE or c == 127:
|
||||
if not self.real_value:
|
||||
self.valstr = ""
|
||||
self.cursor = 0
|
||||
self.real_value = True
|
||||
self.need_update = True
|
||||
elif self.valstr and self.cursor > 0:
|
||||
self.valstr = self.valstr[:self.cursor - 1] + self.valstr[self.cursor:]
|
||||
self.cursor -= 1
|
||||
self.need_update = True
|
||||
elif c == curses.KEY_DC:
|
||||
if not self.real_value:
|
||||
return None
|
||||
if self.valstr and self.cursor < len(self.valstr):
|
||||
self.valstr = self.valstr[:self.cursor] + self.valstr[self.cursor + 1:]
|
||||
self.need_update = True
|
||||
elif c == 45 and self.min_val < 0:
|
||||
if not self.real_value:
|
||||
self.valstr = "-"
|
||||
self.cursor = 1
|
||||
self.real_value = True
|
||||
self.need_update = True
|
||||
if self.cursor != 0:
|
||||
return
|
||||
minus_place = self.valstr.find("-")
|
||||
if minus_place >= 0:
|
||||
return
|
||||
self.valstr = chr(c) + self.valstr
|
||||
self.cursor += 1
|
||||
self.need_update = True
|
||||
elif c > 47 and c < 58:
|
||||
if (not self.real_value) and self.valstr:
|
||||
self.valstr = ""
|
||||
self.cursor = 0
|
||||
self.real_value = True
|
||||
self.need_update = True
|
||||
if c == 48 and self.cursor == 0:
|
||||
return
|
||||
minus_place = self.valstr.find("-")
|
||||
if self.cursor <= minus_place:
|
||||
return
|
||||
if self.cursor == len(self.valstr):
|
||||
self.valstr += chr(c)
|
||||
else:
|
||||
# Insert into string
|
||||
self.valstr = self.valstr[:self.cursor] + chr(c) + self.valstr[self.cursor:]
|
||||
self.need_update = True
|
||||
# Move the cursor forward
|
||||
self.cursor += 1
|
||||
|
||||
def get_value(self):
|
||||
if self.real_value:
|
||||
self.__limit_value()
|
||||
return self.value
|
||||
else:
|
||||
return None
|
||||
|
||||
def set_value(self, val):
|
||||
try:
|
||||
self.value = int(val)
|
||||
self.valstr = "%d" % self.value
|
||||
self.real_value = True
|
||||
except ValueError:
|
||||
self.value = None
|
||||
self.real_value = False
|
||||
self.valstr = val
|
||||
self.cursor = len(self.valstr)
|
||||
|
||||
|
||||
class FloatSpinInput(InputField):
|
||||
def __init__(self, parent, message, name, move_func, value, inc_amt, precision, min_val=None,
|
||||
max_val=None, additional_formatting=False):
|
||||
self.parent = parent
|
||||
self.message = message
|
||||
self.name = name
|
||||
self.precision = precision
|
||||
self.inc_amt = inc_amt
|
||||
|
||||
self.additional_formatting = additional_formatting
|
||||
|
||||
self.fmt = "%%.%df" % precision
|
||||
|
||||
self.default_str = str(value)
|
||||
self.set_value(value)
|
||||
self.default_value = self.value
|
||||
|
||||
self.cursor = len(self.valstr)
|
||||
self.cursoff = colors.get_line_width(self.message) + 4 # + 4 for the " [ " in the rendered string
|
||||
self.move_func = move_func
|
||||
self.min_val = min_val
|
||||
self.max_val = max_val
|
||||
self.need_update = False
|
||||
|
||||
def get_height(self):
|
||||
return 1
|
||||
|
||||
def __limit_value(self):
|
||||
if (self.min_val is not None) and self.value < self.min_val:
|
||||
self.value = self.min_val
|
||||
if (self.max_val is not None) and self.value > self.max_val:
|
||||
self.value = self.max_val
|
||||
self.valstr = self.fmt % self.value
|
||||
|
||||
def render(self, screen, row, width, active, col=1, cursor_offset=0):
|
||||
if not active and self.need_update:
|
||||
if not self.valstr or self.valstr == "-":
|
||||
self.value = self.default_value
|
||||
self.valstr = self.default_str
|
||||
try:
|
||||
float(self.value)
|
||||
except ValueError:
|
||||
self.real_value = False
|
||||
else:
|
||||
self.set_value(self.valstr)
|
||||
self.__limit_value()
|
||||
self.valstr = self.fmt % self.value
|
||||
self.cursor = len(self.valstr)
|
||||
self.cursor = colors.get_line_width(self.valstr)
|
||||
self.need_update = False
|
||||
elif self.need_update and self.valstr != "-":
|
||||
self.real_value = True
|
||||
try:
|
||||
self.value = round(float(self.valstr), self.precision)
|
||||
except ValueError:
|
||||
self.value = self.default_value
|
||||
try:
|
||||
float(self.value)
|
||||
except ValueError:
|
||||
self.real_value = False
|
||||
|
||||
if not self.valstr:
|
||||
self.parent.add_string(row, "%s {!input!}[ ]" % self.message, screen, col, False, True)
|
||||
elif active:
|
||||
self.parent.add_string(row, "%s {!input!}[ {!black,white,bold!}%s{!white,black!} ]" % (
|
||||
self.message, self.valstr), screen, col, False, True)
|
||||
elif self.additional_formatting and self.valstr == self.default_str:
|
||||
self.parent.add_string(row, "%s {!input!}[ {!magenta,black!}%s{!input!} ]" % (
|
||||
self.message, self.valstr), screen, col, False, True)
|
||||
else:
|
||||
self.parent.add_string(row, "%s {!input!}[ %s ]" % (self.message, self.valstr), screen, col, False, True)
|
||||
if active:
|
||||
self.move_func(row, self.cursor + self.cursoff + cursor_offset)
|
||||
|
||||
return 1
|
||||
|
||||
def handle_read(self, c):
|
||||
if c == curses.KEY_PPAGE:
|
||||
if not self.real_value:
|
||||
self.value = self.min_val
|
||||
self.valstr = "%d" % self.value
|
||||
self.real_value = True
|
||||
else:
|
||||
self.value += self.inc_amt
|
||||
self.__limit_value()
|
||||
self.valstr = self.fmt % self.value
|
||||
self.cursor = len(self.valstr)
|
||||
elif c == curses.KEY_NPAGE:
|
||||
if not self.real_value:
|
||||
self.value = self.min_val
|
||||
self.valstr = "%d" % self.value
|
||||
self.real_value = True
|
||||
else:
|
||||
self.value -= self.inc_amt
|
||||
self.__limit_value()
|
||||
self.valstr = self.fmt % self.value
|
||||
self.cursor = len(self.valstr)
|
||||
elif c == curses.KEY_LEFT:
|
||||
if not self.real_value:
|
||||
return None
|
||||
self.cursor = max(0, self.cursor - 1)
|
||||
elif c == curses.KEY_RIGHT:
|
||||
if not self.real_value:
|
||||
return None
|
||||
self.cursor = min(len(self.valstr), self.cursor + 1)
|
||||
elif c == curses.KEY_HOME:
|
||||
if not self.real_value:
|
||||
return None
|
||||
self.cursor = 0
|
||||
elif c == curses.KEY_END:
|
||||
if not self.real_value:
|
||||
return None
|
||||
self.cursor = len(self.valstr)
|
||||
elif c == curses.KEY_BACKSPACE or c == 127:
|
||||
if not self.real_value:
|
||||
self.valstr = ""
|
||||
self.cursor = 0
|
||||
self.real_value = True
|
||||
self.need_update = True
|
||||
elif self.valstr and self.cursor > 0:
|
||||
self.valstr = self.valstr[:self.cursor - 1] + self.valstr[self.cursor:]
|
||||
self.cursor -= 1
|
||||
self.need_update = True
|
||||
elif c == curses.KEY_DC:
|
||||
if not self.real_value:
|
||||
return None
|
||||
if self.valstr and self.cursor < len(self.valstr):
|
||||
self.valstr = self.valstr[:self.cursor] + self.valstr[self.cursor + 1:]
|
||||
self.need_update = True
|
||||
elif c == 45 and self.min_val < 0:
|
||||
if not self.real_value:
|
||||
self.valstr = "-"
|
||||
self.cursor = 1
|
||||
self.need_update = True
|
||||
self.real_value = True
|
||||
if self.cursor != 0:
|
||||
return
|
||||
minus_place = self.valstr.find("-")
|
||||
if minus_place >= 0:
|
||||
return
|
||||
self.valstr = chr(c) + self.valstr
|
||||
self.cursor += 1
|
||||
self.need_update = True
|
||||
elif c == 46:
|
||||
if (not self.real_value) and self.valstr:
|
||||
self.valstr = "0."
|
||||
self.cursor = 2
|
||||
self.real_value = True
|
||||
self.need_update = True
|
||||
minus_place = self.valstr.find("-")
|
||||
if self.cursor <= minus_place:
|
||||
return
|
||||
point_place = self.valstr.find(".")
|
||||
if point_place >= 0:
|
||||
return
|
||||
if self.cursor == len(self.valstr):
|
||||
self.valstr += chr(c)
|
||||
else:
|
||||
# Insert into string
|
||||
self.valstr = self.valstr[:self.cursor] + chr(c) + self.valstr[self.cursor:]
|
||||
self.need_update = True
|
||||
# Move the cursor forward
|
||||
self.cursor += 1
|
||||
elif c > 47 and c < 58:
|
||||
if (not self.real_value) and self.valstr:
|
||||
self.valstr = ""
|
||||
self.cursor = 0
|
||||
self.real_value = True
|
||||
self.need_update = True
|
||||
if self.value == "mixed":
|
||||
self.value = ""
|
||||
minus_place = self.valstr.find("-")
|
||||
if self.cursor <= minus_place:
|
||||
return
|
||||
if self.cursor == len(self.valstr):
|
||||
self.valstr += chr(c)
|
||||
else:
|
||||
# Insert into string
|
||||
self.valstr = self.valstr[:self.cursor] + chr(c) + self.valstr[self.cursor:]
|
||||
self.need_update = True
|
||||
# Move the cursor forward
|
||||
self.cursor += 1
|
||||
|
||||
def get_value(self):
|
||||
if self.real_value:
|
||||
self.__limit_value()
|
||||
return self.value
|
||||
else:
|
||||
return None
|
||||
|
||||
def set_value(self, val):
|
||||
try:
|
||||
self.value = round(float(val), self.precision)
|
||||
self.valstr = self.fmt % self.value
|
||||
self.real_value = True
|
||||
except ValueError:
|
||||
self.value = None
|
||||
self.real_value = False
|
||||
self.valstr = val
|
||||
self.cursor = len(self.valstr)
|
||||
|
||||
|
||||
class SelectInput(InputField):
|
||||
def __init__(self, parent, message, name, opts, vals, selidx, additional_formatting=False):
|
||||
self.parent = parent
|
||||
self.message = message
|
||||
self.additional_formatting = additional_formatting
|
||||
self.name = name
|
||||
self.opts = opts
|
||||
self.vals = vals
|
||||
self.selidx = selidx
|
||||
self.default_option = selidx
|
||||
|
||||
def get_height(self):
|
||||
return 1 + bool(self.message)
|
||||
|
||||
def render(self, screen, row, width, selected, col=1):
|
||||
if self.message:
|
||||
self.parent.add_string(row, self.message, screen, col, False, True)
|
||||
row += 1
|
||||
off = col + 1
|
||||
for i, opt in enumerate(self.opts):
|
||||
if selected and i == self.selidx:
|
||||
self.parent.add_string(row, "{!black,white,bold!}[%s]" % opt, screen, off, False, True)
|
||||
elif i == self.selidx:
|
||||
if self.additional_formatting and i == self.default_option:
|
||||
self.parent.add_string(row, "[{!magenta,black!}%s{!white,black!}]" % opt, screen, off, False, True)
|
||||
elif self.additional_formatting:
|
||||
self.parent.add_string(row, "[{!white,blue!}%s{!white,black!}]" % opt, screen, off, False, True)
|
||||
else:
|
||||
self.parent.add_string(row, "[{!white,black,underline!}%s{!white,black!}]" %
|
||||
opt, screen, off, False, True)
|
||||
else:
|
||||
self.parent.add_string(row, "[%s]" % opt, screen, off, False, True)
|
||||
off += len(opt) + 3
|
||||
if self.message:
|
||||
return 2
|
||||
else:
|
||||
return 1
|
||||
|
||||
def handle_read(self, c):
|
||||
if c == curses.KEY_LEFT:
|
||||
self.selidx = max(0, self.selidx - 1)
|
||||
if c == curses.KEY_RIGHT:
|
||||
self.selidx = min(len(self.opts) - 1, self.selidx + 1)
|
||||
|
||||
def get_value(self):
|
||||
return self.vals[self.selidx]
|
||||
|
||||
def set_value(self, nv):
|
||||
for i, val in enumerate(self.vals):
|
||||
if nv == val:
|
||||
self.selidx = i
|
||||
return
|
||||
raise Exception("Invalid value for SelectInput")
|
||||
|
||||
|
||||
class TextInput(InputField):
|
||||
def __init__(self, parent, move_func, width, message, name, value, docmp, additional_formatting=False):
|
||||
self.parent = parent
|
||||
self.move_func = move_func
|
||||
self.width = width
|
||||
|
||||
self.additional_formatting = additional_formatting
|
||||
|
||||
self.message = message
|
||||
self.name = name
|
||||
self.value = value
|
||||
self.default_value = value
|
||||
self.docmp = docmp
|
||||
|
||||
self.tab_count = 0
|
||||
self.cursor = len(self.value)
|
||||
self.opts = None
|
||||
self.opt_off = 0
|
||||
|
||||
def get_height(self):
|
||||
return 2 + bool(self.message)
|
||||
|
||||
def render(self, screen, row, width, selected, col=1, cursor_offset=0):
|
||||
if not self.value and not selected and len(self.default_value) != 0:
|
||||
self.value = self.default_value
|
||||
self.cursor = len(self.value)
|
||||
|
||||
if self.message:
|
||||
self.parent.add_string(row, self.message, screen, col, False, True)
|
||||
row += 1
|
||||
if selected:
|
||||
if self.opts:
|
||||
self.parent.add_string(row + 1, self.opts[self.opt_off:], screen, col, False, True)
|
||||
if self.cursor > (width - 3):
|
||||
self.move_func(row, width - 2)
|
||||
else:
|
||||
self.move_func(row, self.cursor + 1 + cursor_offset)
|
||||
slen = len(self.value) + 3
|
||||
if slen > width:
|
||||
vstr = self.value[(slen - width):]
|
||||
else:
|
||||
vstr = self.value.ljust(width - 2)
|
||||
|
||||
if self.additional_formatting and len(self.value) != 0 and self.value == self.default_value:
|
||||
self.parent.add_string(row, "{!magenta,white!}%s" % vstr, screen, col, False, False)
|
||||
else:
|
||||
self.parent.add_string(row, "{!black,white,bold!}%s" % vstr, screen, col, False, False)
|
||||
|
||||
if self.message:
|
||||
return 3
|
||||
else:
|
||||
return 2
|
||||
|
||||
def get_value(self):
|
||||
return self.value
|
||||
|
||||
def set_value(self, val):
|
||||
self.value = val
|
||||
self.cursor = len(self.value)
|
||||
|
||||
# most of the cursor,input stuff here taken from ui/console/screen.py
|
||||
def handle_read(self, c):
|
||||
if c == 9 and self.docmp:
|
||||
# Keep track of tab hit count to know when it's double-hit
|
||||
self.tab_count += 1
|
||||
if self.tab_count > 1:
|
||||
second_hit = True
|
||||
self.tab_count = 0
|
||||
else:
|
||||
second_hit = False
|
||||
|
||||
# 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.cursor == len(self.value) or self.value[self.cursor] == " ":
|
||||
if self.opts:
|
||||
prev = self.opt_off
|
||||
self.opt_off += self.width - 3
|
||||
# now find previous double space, best guess at a split point
|
||||
# in future could keep opts unjoined to get this really right
|
||||
self.opt_off = self.opts.rfind(" ", 0, self.opt_off) + 2
|
||||
if second_hit and self.opt_off == prev: # double tap and we're at the end
|
||||
self.opt_off = 0
|
||||
else:
|
||||
opts = self.complete(self.value)
|
||||
if len(opts) == 1: # only one option, just complete it
|
||||
self.value = opts[0]
|
||||
self.cursor = len(opts[0])
|
||||
self.tab_count = 0
|
||||
elif len(opts) > 1:
|
||||
prefix = os.path.commonprefix(opts)
|
||||
if prefix:
|
||||
self.value = prefix
|
||||
self.cursor = len(prefix)
|
||||
|
||||
if len(opts) > 1 and second_hit: # display multiple options on second tab hit
|
||||
sp = self.value.rfind(os.sep) + 1
|
||||
self.opts = " ".join([o[sp:] for o in opts])
|
||||
|
||||
# Cursor movement
|
||||
elif c == curses.KEY_LEFT:
|
||||
self.cursor = max(0, self.cursor - 1)
|
||||
elif c == curses.KEY_RIGHT:
|
||||
self.cursor = min(len(self.value), self.cursor + 1)
|
||||
elif c == curses.KEY_HOME:
|
||||
self.cursor = 0
|
||||
elif c == curses.KEY_END:
|
||||
self.cursor = len(self.value)
|
||||
|
||||
if c != 9:
|
||||
self.opts = None
|
||||
self.opt_off = 0
|
||||
self.tab_count = 0
|
||||
|
||||
# Delete a character in the input string based on cursor position
|
||||
if c == curses.KEY_BACKSPACE or c == 127:
|
||||
if self.value and self.cursor > 0:
|
||||
self.value = self.value[:self.cursor - 1] + self.value[self.cursor:]
|
||||
self.cursor -= 1
|
||||
elif c == curses.KEY_DC:
|
||||
if self.value and self.cursor < len(self.value):
|
||||
self.value = self.value[:self.cursor] + self.value[self.cursor + 1:]
|
||||
elif c > 31 and c < 256:
|
||||
# Emulate getwch
|
||||
stroke = chr(c)
|
||||
uchar = ""
|
||||
while not uchar:
|
||||
try:
|
||||
uchar = stroke.decode(self.parent.encoding)
|
||||
except UnicodeDecodeError:
|
||||
c = self.parent.parent.stdscr.getch()
|
||||
stroke += chr(c)
|
||||
if uchar:
|
||||
if self.cursor == len(self.value):
|
||||
self.value += uchar
|
||||
else:
|
||||
# Insert into string
|
||||
self.value = self.value[:self.cursor] + uchar + self.value[self.cursor:]
|
||||
# Move the cursor forward
|
||||
self.cursor += 1
|
||||
|
||||
def complete(self, line):
|
||||
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))
|
||||
for f in os.listdir(line):
|
||||
# Skip hidden
|
||||
if f.startswith("."):
|
||||
continue
|
||||
f = os.path.join(line, f)
|
||||
if os.path.isdir(f):
|
||||
f += os.sep
|
||||
ret.append(f)
|
||||
else:
|
||||
# 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))
|
||||
else:
|
||||
# This path does not exist, so lets do a listdir on it's parent
|
||||
# and find any matches.
|
||||
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):
|
||||
p += os.sep
|
||||
ret.append(p)
|
||||
return ret
|
||||
|
||||
|
||||
class InputPopup(Popup):
|
||||
def __init__(self, parent_mode, title, width_req=0, height_req=0, align=ALIGN.DEFAULT, close_cb=None,
|
||||
additional_formatting=True, immediate_action=False):
|
||||
Popup.__init__(self, parent_mode, title, width_req=width_req, height_req=height_req,
|
||||
align=align, close_cb=close_cb)
|
||||
self.inputs = []
|
||||
self.lines = []
|
||||
self.current_input = 0
|
||||
|
||||
self.additional_formatting = additional_formatting
|
||||
self.immediate_action = immediate_action
|
||||
|
||||
# We need to replicate some things in order to wrap our inputs
|
||||
self.encoding = parent_mode.encoding
|
||||
|
||||
def move(self, r, c):
|
||||
self._cursor_row = r
|
||||
self._cursor_col = c
|
||||
|
||||
def add_text_input(self, message, name, value="", complete=True):
|
||||
"""
|
||||
Add a text input field to the popup.
|
||||
|
||||
:param message: string to display above the input field
|
||||
:param name: name of the field, for the return callback
|
||||
:param value: initial value of the field
|
||||
:param complete: should completion be run when tab is hit and this field is active
|
||||
"""
|
||||
self.inputs.append(TextInput(self, self.move, self.width, message, name, value, complete,
|
||||
additional_formatting=self.additional_formatting))
|
||||
|
||||
def getmaxyx(self):
|
||||
return self.screen.getmaxyx()
|
||||
|
||||
def add_string(self, row, string, scr=None, col=0, pad=True, trim=True):
|
||||
if row <= 0:
|
||||
return False
|
||||
elif row >= self.height - 1:
|
||||
return False
|
||||
self.parent.add_string(row, string, scr, col, pad, trim)
|
||||
return True
|
||||
|
||||
def add_spaces(self, num):
|
||||
for i in range(num):
|
||||
self.lines.append((len(self.inputs), ""))
|
||||
|
||||
def add_text(self, string):
|
||||
lines = string.split("\n")
|
||||
for line in lines:
|
||||
self.lines.append((len(self.inputs), line))
|
||||
|
||||
def add_select_input(self, message, name, opts, vals, default_index=0):
|
||||
self.inputs.append(SelectInput(self, message, name, opts, vals, default_index,
|
||||
additional_formatting=self.additional_formatting))
|
||||
|
||||
def add_checked_input(self, message, name, checked=False):
|
||||
self.inputs.append(CheckedInput(self, message, name, checked,
|
||||
additional_formatting=self.additional_formatting))
|
||||
|
||||
# def add_checked_plus_input(self, message, name, child)
|
||||
|
||||
def add_float_spin_input(self, message, name, value=0.0, inc_amt=1.0, precision=1, min_val=None, max_val=None):
|
||||
i = FloatSpinInput(self, message, name, self.move, value, inc_amt, precision, min_val, max_val,
|
||||
additional_formatting=self.additional_formatting)
|
||||
self.inputs.append(i)
|
||||
|
||||
def add_int_spin_input(self, message, name, value=0, min_val=None, max_val=None):
|
||||
i = IntSpinInput(self, message, name, self.move, value, min_val, max_val,
|
||||
additional_formatting=self.additional_formatting)
|
||||
self.inputs.append(i)
|
||||
|
||||
def _refresh_lines(self):
|
||||
self._cursor_row = -1
|
||||
self._cursor_col = -1
|
||||
curses.curs_set(0)
|
||||
|
||||
start_row = 0
|
||||
end_row = 0
|
||||
for i, ipt in enumerate(self.inputs):
|
||||
for line in self.lines:
|
||||
if line[0] == i:
|
||||
end_row += 1
|
||||
start_row = end_row
|
||||
end_row += ipt.get_height()
|
||||
active = (i == self.current_input)
|
||||
|
||||
if active:
|
||||
if end_row + 1 >= self.height + self.lineoff:
|
||||
self.lineoff += ipt.get_height()
|
||||
elif start_row < self.lineoff:
|
||||
self.lineoff -= ipt.get_height()
|
||||
self.content_height = end_row
|
||||
|
||||
crow = 1 - self.lineoff
|
||||
for i, ipt in enumerate(self.inputs):
|
||||
for line in self.lines:
|
||||
if line[0] == i:
|
||||
self.add_string(crow, line[1], self.screen, 1, pad=False)
|
||||
crow += 1
|
||||
crow += ipt.render(self.screen, crow, self.width, i == self.current_input)
|
||||
|
||||
if self.content_height > (self.height - 2):
|
||||
lts = self.content_height - (self.height - 3)
|
||||
perc_sc = self.lineoff / lts
|
||||
sb_pos = int((self.height - 2) * perc_sc) + 1
|
||||
if (sb_pos == 1) and (self.lineoff != 0):
|
||||
sb_pos += 1
|
||||
self.add_string(sb_pos, "{!red,black,bold!}#", self.screen, col=(self.width - 1), pad=False, trim=False)
|
||||
if self._cursor_row >= 0:
|
||||
curses.curs_set(2)
|
||||
self.screen.move(self._cursor_row, self._cursor_col)
|
||||
|
||||
def handle_read(self, c):
|
||||
if c == curses.KEY_UP:
|
||||
self.current_input = max(0, self.current_input - 1)
|
||||
elif c == curses.KEY_DOWN:
|
||||
self.current_input = min(len(self.inputs) - 1, self.current_input + 1)
|
||||
elif c == curses.KEY_ENTER or c == 10:
|
||||
if self.close_cb:
|
||||
vals = {}
|
||||
for ipt in self.inputs:
|
||||
vals[ipt.name] = ipt.get_value()
|
||||
curses.curs_set(0)
|
||||
self.close_cb(vals)
|
||||
return True # close the popup
|
||||
elif c == 27: # close on esc, no action
|
||||
return True
|
||||
elif self.inputs:
|
||||
self.inputs[self.current_input].handle_read(c)
|
||||
if self.immediate_action:
|
||||
vals = {}
|
||||
for ipt in self.inputs:
|
||||
vals[ipt.name] = ipt.get_value()
|
||||
self.close_cb(vals)
|
||||
|
||||
self.refresh()
|
||||
return False
|
@ -1,350 +0,0 @@
|
||||
# -*- 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 division
|
||||
|
||||
import logging
|
||||
|
||||
from deluge.ui.console.modes import format_utils
|
||||
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ALIGN(object):
|
||||
TOP_LEFT = 1
|
||||
TOP_CENTER = 2
|
||||
TOP_RIGHT = 3
|
||||
MIDDLE_LEFT = 4
|
||||
MIDDLE_CENTER = 5
|
||||
MIDDLE_RIGHT = 6
|
||||
BOTTOM_LEFT = 7
|
||||
BOTTOM_CENTER = 8
|
||||
BOTTOM_RIGHT = 9
|
||||
DEFAULT = MIDDLE_CENTER
|
||||
|
||||
|
||||
class Popup(object):
|
||||
def __init__(self, parent_mode, title, width_req=0, height_req=0, align=ALIGN.DEFAULT,
|
||||
close_cb=None, init_lines=None):
|
||||
"""
|
||||
Init a new popup. The default constructor will handle sizing and borders and the like.
|
||||
|
||||
NB: The parent mode is responsible for calling refresh on any popups it wants to show.
|
||||
This should be called as the last thing in the parents refresh method.
|
||||
|
||||
The parent *must* also call read_input on the popup instead of/in addition to
|
||||
running its own read_input code if it wants to have the popup handle user input.
|
||||
|
||||
:param parent_mode: must be a basemode (or subclass) which the popup will be drawn over
|
||||
:parem title: string, the title of the popup window
|
||||
|
||||
Popups have two methods that must be implemented:
|
||||
|
||||
refresh(self) - draw the popup window to screen. this default mode simply draws a bordered window
|
||||
with the supplied title to the screen
|
||||
|
||||
add_string(self, row, string) - add string at row. handles triming/ignoring if the string won't fit in the popup
|
||||
|
||||
read_input(self) - handle user input to the popup.
|
||||
"""
|
||||
self.parent = parent_mode
|
||||
|
||||
self.height_req = height_req
|
||||
self.width_req = width_req
|
||||
self.align = align
|
||||
|
||||
self.handle_resize()
|
||||
|
||||
self.title = title
|
||||
self.close_cb = close_cb
|
||||
|
||||
self.divider = None
|
||||
self.lineoff = 0
|
||||
if init_lines:
|
||||
self._lines = init_lines
|
||||
else:
|
||||
self._lines = []
|
||||
|
||||
def _refresh_lines(self):
|
||||
crow = 1
|
||||
for line in self._lines[self.lineoff:]:
|
||||
if crow >= self.height - 1:
|
||||
break
|
||||
self.parent.add_string(crow, line, self.screen, 1, False, True)
|
||||
crow += 1
|
||||
|
||||
def handle_resize(self):
|
||||
if isinstance(self.height_req, float) and 0.0 < self.height_req <= 1.0:
|
||||
hr = int((self.parent.rows - 2) * self.height_req)
|
||||
else:
|
||||
hr = self.height_req
|
||||
|
||||
if isinstance(self.width_req, float) and 0.0 < self.width_req <= 1.0:
|
||||
wr = int((self.parent.cols - 2) * self.width_req)
|
||||
else:
|
||||
wr = self.width_req
|
||||
|
||||
log.debug("Resizing(or creating) popup window")
|
||||
|
||||
# Height
|
||||
if hr == 0:
|
||||
hr = self.parent.rows // 2
|
||||
elif hr == -1:
|
||||
hr = self.parent.rows - 2
|
||||
elif hr > self.parent.rows - 2:
|
||||
hr = self.parent.rows - 2
|
||||
|
||||
# Width
|
||||
if wr == 0:
|
||||
wr = self.parent.cols // 2
|
||||
elif wr == -1:
|
||||
wr = self.parent.cols
|
||||
elif wr >= self.parent.cols:
|
||||
wr = self.parent.cols
|
||||
|
||||
if self.align in [ALIGN.TOP_CENTER, ALIGN.TOP_LEFT, ALIGN.TOP_RIGHT]:
|
||||
by = 1
|
||||
elif self.align in [ALIGN.MIDDLE_CENTER, ALIGN.MIDDLE_LEFT, ALIGN.MIDDLE_RIGHT]:
|
||||
by = (self.parent.rows // 2) - (hr // 2)
|
||||
elif self.align in [ALIGN.BOTTOM_CENTER, ALIGN.BOTTOM_LEFT, ALIGN.BOTTOM_RIGHT]:
|
||||
by = self.parent.rows - hr - 1
|
||||
|
||||
if self.align in [ALIGN.TOP_LEFT, ALIGN.MIDDLE_LEFT, ALIGN.BOTTOM_LEFT]:
|
||||
bx = 0
|
||||
elif self.align in [ALIGN.TOP_CENTER, ALIGN.MIDDLE_CENTER, ALIGN.BOTTOM_CENTER]:
|
||||
bx = (self.parent.cols // 2) - (wr // 2)
|
||||
elif self.align in [ALIGN.TOP_RIGHT, ALIGN.MIDDLE_RIGHT, ALIGN.BOTTOM_RIGHT]:
|
||||
bx = self.parent.cols - wr - 1
|
||||
|
||||
self.screen = curses.newwin(hr, wr, by, bx)
|
||||
|
||||
self.x, self.y = bx, by
|
||||
self.height, self.width = self.screen.getmaxyx()
|
||||
|
||||
def refresh(self):
|
||||
self.screen.erase()
|
||||
self.screen.border(0, 0, 0, 0)
|
||||
toff = max(1, (self.width // 2) - (len(self.title) // 2))
|
||||
self.parent.add_string(0, "{!white,black,bold!}%s" % self.title, self.screen, toff, False, True)
|
||||
|
||||
self._refresh_lines()
|
||||
if len(self._lines) > (self.height - 2):
|
||||
lts = len(self._lines) - (self.height - 3)
|
||||
perc_sc = self.lineoff / lts
|
||||
sb_pos = int((self.height - 2) * perc_sc) + 1
|
||||
if (sb_pos == 1) and (self.lineoff != 0):
|
||||
sb_pos += 1
|
||||
self.parent.add_string(sb_pos, "{!red,black,bold!}#", self.screen, col=(self.width - 1),
|
||||
pad=False, trim=False)
|
||||
|
||||
self.screen.redrawwin()
|
||||
self.screen.noutrefresh()
|
||||
|
||||
def clear(self):
|
||||
self._lines = []
|
||||
|
||||
def handle_read(self, c):
|
||||
p_off = self.height - 3
|
||||
if c == curses.KEY_UP:
|
||||
self.lineoff = max(0, self.lineoff - 1)
|
||||
elif c == curses.KEY_PPAGE:
|
||||
self.lineoff = max(0, self.lineoff - p_off)
|
||||
elif c == curses.KEY_HOME:
|
||||
self.lineoff = 0
|
||||
elif c == curses.KEY_DOWN:
|
||||
if len(self._lines) - self.lineoff > (self.height - 2):
|
||||
self.lineoff += 1
|
||||
elif c == curses.KEY_NPAGE:
|
||||
self.lineoff = min(len(self._lines) - self.height + 2, self.lineoff + p_off)
|
||||
elif c == curses.KEY_END:
|
||||
self.lineoff = len(self._lines) - self.height + 2
|
||||
|
||||
elif c == curses.KEY_ENTER or c == 10 or c == 27: # close on enter/esc
|
||||
if self.close_cb:
|
||||
self.close_cb()
|
||||
return True # close the popup
|
||||
|
||||
if c > 31 and c < 256 and chr(c) == "q":
|
||||
if self.close_cb:
|
||||
self.close_cb()
|
||||
return True # close the popup
|
||||
|
||||
self.refresh()
|
||||
|
||||
return False
|
||||
|
||||
def set_title(self, title):
|
||||
self.title = title
|
||||
|
||||
def add_line(self, string):
|
||||
self._lines.append(string)
|
||||
|
||||
def add_divider(self):
|
||||
if not self.divider:
|
||||
self.divider = "-" * (self.width - 2)
|
||||
self._lines.append(self.divider)
|
||||
|
||||
|
||||
class SelectablePopup(Popup):
|
||||
"""
|
||||
A popup which will let the user select from some of the lines that
|
||||
are added.
|
||||
"""
|
||||
def __init__(self, parent_mode, title, selection_callback, args=(), align=ALIGN.DEFAULT, immediate_action=False):
|
||||
Popup.__init__(self, parent_mode, title, align=align)
|
||||
self._selection_callback = selection_callback
|
||||
self._selection_args = args
|
||||
self._selectable_lines = []
|
||||
|
||||
self._immediate_action = immediate_action
|
||||
|
||||
self._select_data = []
|
||||
self._line_foregrounds = []
|
||||
self._udxs = {}
|
||||
self._hotkeys = {}
|
||||
self._selected = -1
|
||||
|
||||
def add_line(self, string, selectable=True, use_underline=True, data=None, foreground=None):
|
||||
if use_underline:
|
||||
udx = string.find("_")
|
||||
if udx >= 0:
|
||||
string = string[:udx] + string[udx + 1:]
|
||||
self._udxs[len(self._lines) + 1] = udx
|
||||
c = string[udx].lower()
|
||||
self._hotkeys[c] = len(self._lines)
|
||||
Popup.add_line(self, string)
|
||||
self._line_foregrounds.append(foreground)
|
||||
if selectable:
|
||||
self._selectable_lines.append(len(self._lines) - 1)
|
||||
self._select_data.append(data)
|
||||
if self._selected < 0:
|
||||
self._selected = (len(self._lines) - 1)
|
||||
|
||||
def _refresh_lines(self):
|
||||
crow = 1
|
||||
for row, line in enumerate(self._lines):
|
||||
if crow >= self.height - 1:
|
||||
break
|
||||
if row < self.lineoff:
|
||||
continue
|
||||
fg = self._line_foregrounds[row]
|
||||
udx = self._udxs.get(crow)
|
||||
if row == self._selected:
|
||||
if fg is None:
|
||||
fg = "black"
|
||||
colorstr = "{!%s,white,bold!}" % fg
|
||||
if udx >= 0:
|
||||
ustr = "{!%s,white,bold,underline!}" % fg
|
||||
else:
|
||||
if fg is None:
|
||||
fg = "white"
|
||||
colorstr = "{!%s,black!}" % fg
|
||||
if udx >= 0:
|
||||
ustr = "{!%s,black,underline!}" % fg
|
||||
if udx == 0:
|
||||
self.parent.add_string(crow, "- %s%c%s%s" % (
|
||||
ustr, line[0], colorstr, line[1:]), self.screen, 1, False, True)
|
||||
elif udx > 0:
|
||||
# well, this is a litte gross
|
||||
self.parent.add_string(crow, "- %s%s%s%c%s%s" % (
|
||||
colorstr, line[:udx], ustr, line[udx], colorstr, line[udx + 1:]), self.screen, 1, False, True)
|
||||
else:
|
||||
self.parent.add_string(crow, "- %s%s" % (colorstr, line), self.screen, 1, False, True)
|
||||
crow += 1
|
||||
|
||||
def current_selection(self):
|
||||
"Returns a tuple of (selected index, selected data)"
|
||||
idx = self._selectable_lines.index(self._selected)
|
||||
return (idx, self._select_data[idx])
|
||||
|
||||
def add_divider(self, color="white"):
|
||||
if not self.divider:
|
||||
self.divider = "-" * (self.width - 6) + " -"
|
||||
self._lines.append(self.divider)
|
||||
self._line_foregrounds.append(color)
|
||||
|
||||
def _move_cursor_up(self, amount):
|
||||
if self._selectable_lines.index(self._selected) > amount:
|
||||
idx = self._selectable_lines.index(self._selected)
|
||||
self._selected = self._selectable_lines[idx - amount]
|
||||
else:
|
||||
self._selected = self._selectable_lines[0]
|
||||
|
||||
if self._immediate_action:
|
||||
self._selection_callback(idx, self._select_data[idx], *self._selection_args)
|
||||
|
||||
def _move_cursor_down(self, amount):
|
||||
idx = self._selectable_lines.index(self._selected)
|
||||
if idx < len(self._selectable_lines) - amount:
|
||||
self._selected = self._selectable_lines[idx + amount]
|
||||
else:
|
||||
self._selected = self._selectable_lines[-1]
|
||||
|
||||
if self._immediate_action:
|
||||
self._selection_callback(idx, self._select_data[idx], *self._selection_args)
|
||||
|
||||
def handle_read(self, c):
|
||||
if c == curses.KEY_UP:
|
||||
self._move_cursor_up(1)
|
||||
elif c == curses.KEY_DOWN:
|
||||
self._move_cursor_down(1)
|
||||
|
||||
elif c == curses.KEY_PPAGE:
|
||||
self._move_cursor_up(4)
|
||||
elif c == curses.KEY_NPAGE:
|
||||
self._move_cursor_down(4)
|
||||
|
||||
elif c == curses.KEY_HOME:
|
||||
self._move_cursor_up(len(self._selectable_lines))
|
||||
elif c == curses.KEY_END:
|
||||
self._move_cursor_down(len(self._selectable_lines))
|
||||
|
||||
elif c == 27: # close on esc, no action
|
||||
return True
|
||||
|
||||
elif c == curses.KEY_ENTER or c == 10:
|
||||
idx = self._selectable_lines.index(self._selected)
|
||||
return self._selection_callback(idx, self._select_data[idx], *self._selection_args)
|
||||
|
||||
if c > 31 and c < 256:
|
||||
if chr(c) == "q":
|
||||
return True # close the popup
|
||||
uc = chr(c).lower()
|
||||
if uc in self._hotkeys:
|
||||
# exec hotkey action
|
||||
idx = self._selectable_lines.index(self._hotkeys[uc])
|
||||
return self._selection_callback(idx, self._select_data[idx], *self._selection_args)
|
||||
self.refresh()
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class MessagePopup(Popup):
|
||||
"""
|
||||
Popup that just displays a message
|
||||
"""
|
||||
def __init__(self, parent_mode, title, message, align=ALIGN.DEFAULT, width_req=0.5):
|
||||
self.message = message
|
||||
# self.width= int(parent_mode.cols/2)
|
||||
Popup.__init__(self, parent_mode, title, align=align, width_req=width_req)
|
||||
lns = format_utils.wrap_string(self.message, self.width - 2, 3, True)
|
||||
self.height_req = min(len(lns) + 2, int(parent_mode.rows * 2 / 3))
|
||||
self.handle_resize()
|
||||
self._lines = lns
|
||||
|
||||
def handle_resize(self):
|
||||
Popup.handle_resize(self)
|
||||
self.clear()
|
||||
self._lines = format_utils.wrap_string(self.message, self.width - 2, 3, True)
|
@ -1,490 +0,0 @@
|
||||
# -*- 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.
|
||||
#
|
||||
|
||||
import logging
|
||||
|
||||
import deluge.ui.console.modes.alltorrents
|
||||
from deluge.common import is_ip
|
||||
from deluge.ui.console.modes.input_popup import (CheckedInput, CheckedPlusInput, FloatSpinInput, IntSpinInput,
|
||||
SelectInput, TextInput)
|
||||
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NoInput(object):
|
||||
def depend_skip(self):
|
||||
return False
|
||||
|
||||
|
||||
class Header(NoInput):
|
||||
def __init__(self, parent, header, space_above, space_below):
|
||||
self.parent = parent
|
||||
self.header = "{!white,black,bold!}%s" % header
|
||||
self.space_above = space_above
|
||||
self.space_below = space_below
|
||||
self.name = header
|
||||
|
||||
def render(self, screen, row, width, active, offset):
|
||||
rows = 1
|
||||
if self.space_above:
|
||||
row += 1
|
||||
rows += 1
|
||||
self.parent.add_string(row, self.header, screen, offset - 1, False, True)
|
||||
if self.space_below:
|
||||
rows += 1
|
||||
return rows
|
||||
|
||||
|
||||
class InfoField(NoInput):
|
||||
def __init__(self, parent, label, value, name):
|
||||
self.parent = parent
|
||||
self.label = label
|
||||
self.value = value
|
||||
self.txt = "%s %s" % (label, value)
|
||||
self.name = name
|
||||
|
||||
def render(self, screen, row, width, active, offset):
|
||||
self.parent.add_string(row, self.txt, screen, offset - 1, False, True)
|
||||
return 1
|
||||
|
||||
def set_value(self, v):
|
||||
self.value = v
|
||||
if isinstance(v, float):
|
||||
self.txt = "%s %.2f" % (self.label, self.value)
|
||||
else:
|
||||
self.txt = "%s %s" % (self.label, self.value)
|
||||
|
||||
|
||||
class BasePane(object):
|
||||
def __init__(self, offset, parent, width):
|
||||
self.offset = offset + 1
|
||||
self.parent = parent
|
||||
self.width = width
|
||||
self.inputs = []
|
||||
self.active_input = -1
|
||||
|
||||
# have we scrolled down in the list
|
||||
self.input_offset = 0
|
||||
|
||||
def move(self, r, c):
|
||||
self._cursor_row = r
|
||||
self._cursor_col = c
|
||||
|
||||
def add_config_values(self, conf_dict):
|
||||
for ipt in self.inputs:
|
||||
if not isinstance(ipt, NoInput):
|
||||
# gross, have to special case in/out ports since they are tuples
|
||||
if ipt.name in ("listen_interface", "listen_port", "out_ports_from", "out_ports_to",
|
||||
"i2p_port", "i2p_hostname", "proxy_type", "proxy_username", "proxy_hostnames",
|
||||
"proxy_password", "proxy_hostname", "proxy_port", "proxy_peer_connections"):
|
||||
if ipt.name == "listen_port":
|
||||
conf_dict["listen_ports"] = [self.infrom.get_value()] * 2
|
||||
elif ipt.name == "out_ports_to":
|
||||
conf_dict["outgoing_ports"] = (self.outfrom.get_value(), self.outto.get_value())
|
||||
elif ipt.name == "listen_interface":
|
||||
interface = ipt.get_value().strip()
|
||||
if is_ip(interface) or not interface:
|
||||
conf_dict["listen_interface"] = interface
|
||||
elif ipt.name == "i2p_port":
|
||||
conf_dict.setdefault("i2p_proxy", {})["port"] = ipt.get_value()
|
||||
elif ipt.name == "i2p_hostname":
|
||||
conf_dict.setdefault("i2p_proxy", {})["hostname"] = ipt.get_value()
|
||||
elif ipt.name == "proxy_type":
|
||||
conf_dict.setdefault("proxy", {})["type"] = ipt.get_value()
|
||||
elif ipt.name == "proxy_username":
|
||||
conf_dict.setdefault("proxy", {})["username"] = ipt.get_value()
|
||||
elif ipt.name == "proxy_password":
|
||||
conf_dict.setdefault("proxy", {})["password"] = ipt.get_value()
|
||||
elif ipt.name == "proxy_hostname":
|
||||
conf_dict.setdefault("proxy", {})["hostname"] = ipt.get_value()
|
||||
elif ipt.name == "proxy_port":
|
||||
conf_dict.setdefault("proxy", {})["port"] = ipt.get_value()
|
||||
elif ipt.name == "proxy_hostnames":
|
||||
conf_dict.setdefault("proxy", {})["proxy_hostnames"] = ipt.get_value()
|
||||
elif ipt.name == "proxy_peer_connections":
|
||||
conf_dict.setdefault("proxy", {})["proxy_peer_connections"] = ipt.get_value()
|
||||
else:
|
||||
conf_dict[ipt.name] = ipt.get_value()
|
||||
if hasattr(ipt, "get_child"):
|
||||
c = ipt.get_child()
|
||||
conf_dict[c.name] = c.get_value()
|
||||
|
||||
def update_values(self, conf_dict):
|
||||
for ipt in self.inputs:
|
||||
if not isinstance(ipt, NoInput):
|
||||
try:
|
||||
ipt.set_value(conf_dict[ipt.name])
|
||||
except KeyError: # just ignore if it's not in dict
|
||||
pass
|
||||
if hasattr(ipt, "get_child"):
|
||||
try:
|
||||
c = ipt.get_child()
|
||||
c.set_value(conf_dict[c.name])
|
||||
except KeyError: # just ignore if it's not in dict
|
||||
pass
|
||||
|
||||
def render(self, mode, screen, width, active):
|
||||
self._cursor_row = -1
|
||||
if self.active_input < 0:
|
||||
for i, ipt in enumerate(self.inputs):
|
||||
if not isinstance(ipt, NoInput):
|
||||
self.active_input = i
|
||||
break
|
||||
drew_act = not active
|
||||
crow = 1
|
||||
for i, ipt in enumerate(self.inputs):
|
||||
if ipt.depend_skip() or i < self.input_offset:
|
||||
if active and i == self.active_input:
|
||||
self.input_offset -= 1
|
||||
mode.refresh()
|
||||
return 0
|
||||
continue
|
||||
act = active and i == self.active_input
|
||||
if act:
|
||||
drew_act = True
|
||||
crow += ipt.render(screen, crow, width, act, self.offset)
|
||||
if crow >= (mode.prefs_height):
|
||||
break
|
||||
|
||||
if not drew_act:
|
||||
self.input_offset += 1
|
||||
mode.refresh()
|
||||
return 0
|
||||
|
||||
if active and self._cursor_row >= 0:
|
||||
curses.curs_set(2)
|
||||
screen.move(self._cursor_row, self._cursor_col + self.offset - 1)
|
||||
else:
|
||||
curses.curs_set(0)
|
||||
|
||||
return crow
|
||||
|
||||
# just handles setting the active input
|
||||
def handle_read(self, c):
|
||||
if not self.inputs: # no inputs added yet
|
||||
return
|
||||
|
||||
if c == curses.KEY_UP:
|
||||
nc = max(0, self.active_input - 1)
|
||||
while isinstance(self.inputs[nc], NoInput) or self.inputs[nc].depend_skip():
|
||||
nc -= 1
|
||||
if nc <= 0:
|
||||
break
|
||||
if not isinstance(self.inputs[nc], NoInput) or self.inputs[nc].depend_skip():
|
||||
self.active_input = nc
|
||||
elif c == curses.KEY_DOWN:
|
||||
ilen = len(self.inputs)
|
||||
nc = min(self.active_input + 1, ilen - 1)
|
||||
while isinstance(self.inputs[nc], NoInput) or self.inputs[nc].depend_skip():
|
||||
nc += 1
|
||||
if nc >= ilen:
|
||||
nc -= 1
|
||||
break
|
||||
if not isinstance(self.inputs[nc], NoInput) or self.inputs[nc].depend_skip():
|
||||
self.active_input = nc
|
||||
else:
|
||||
self.inputs[self.active_input].handle_read(c)
|
||||
|
||||
def add_header(self, header, space_above=False, space_below=False):
|
||||
self.inputs.append(Header(self.parent, header, space_above, space_below))
|
||||
|
||||
def add_info_field(self, label, value, name):
|
||||
self.inputs.append(InfoField(self.parent, label, value, name))
|
||||
|
||||
def add_text_input(self, name, msg, dflt_val):
|
||||
self.inputs.append(TextInput(self.parent, self.move, self.width, msg, name, dflt_val, False))
|
||||
|
||||
def add_select_input(self, name, msg, opts, vals, selidx):
|
||||
self.inputs.append(SelectInput(self.parent, msg, name, opts, vals, selidx))
|
||||
|
||||
def add_checked_input(self, name, message, checked):
|
||||
self.inputs.append(CheckedInput(self.parent, message, name, checked))
|
||||
|
||||
def add_checkedplus_input(self, name, message, child, checked):
|
||||
self.inputs.append(CheckedPlusInput(self.parent, message, name, child, checked))
|
||||
|
||||
def add_int_spin_input(self, name, message, value, min_val, max_val):
|
||||
self.inputs.append(IntSpinInput(self.parent, message, name, self.move, value, min_val, max_val))
|
||||
|
||||
def add_float_spin_input(self, name, message, value, inc_amt, precision, min_val, max_val):
|
||||
self.inputs.append(FloatSpinInput(self.parent, message, name, self.move, value,
|
||||
inc_amt, precision, min_val, max_val))
|
||||
|
||||
|
||||
class InterfacePane(BasePane):
|
||||
def __init__(self, offset, parent, width):
|
||||
BasePane.__init__(self, offset, parent, width)
|
||||
self.add_header("General options", False)
|
||||
|
||||
self.add_checked_input("ring_bell", "Ring system bell when a download finishes",
|
||||
parent.console_config["ring_bell"])
|
||||
|
||||
self.add_header("New Console UI", True)
|
||||
|
||||
self.add_checked_input("separate_complete",
|
||||
"List complete torrents after incomplete regardless of sorting order",
|
||||
parent.console_config["separate_complete"])
|
||||
self.add_checked_input("move_selection", "Move selection when moving torrents in the queue",
|
||||
parent.console_config["move_selection"])
|
||||
|
||||
self.add_header("Legacy Mode", True)
|
||||
|
||||
self.add_checked_input("ignore_duplicate_lines", "Do not store duplicate input in history",
|
||||
parent.console_config["ignore_duplicate_lines"])
|
||||
self.add_checked_input("save_legacy_history", "Store and load command line history in Legacy mode",
|
||||
parent.console_config["save_legacy_history"])
|
||||
|
||||
self.add_header("", False)
|
||||
|
||||
self.add_checked_input("third_tab_lists_all", "Third tab lists all remaining torrents in legacy mode",
|
||||
parent.console_config["third_tab_lists_all"])
|
||||
self.add_int_spin_input("torrents_per_tab_press", "Torrents per tab press",
|
||||
parent.console_config["torrents_per_tab_press"], 5, 100)
|
||||
|
||||
|
||||
class ColumnsPane(BasePane):
|
||||
def __init__(self, offset, parent, width):
|
||||
BasePane.__init__(self, offset, parent, width)
|
||||
self.add_header("Columns To Display", True)
|
||||
|
||||
default_prefs = deluge.ui.console.modes.alltorrents.DEFAULT_PREFS
|
||||
|
||||
for cpn in deluge.ui.console.modes.alltorrents.column_pref_names:
|
||||
pn = "show_%s" % cpn
|
||||
# If there is no option for it, it's not togglable
|
||||
# We check in default_prefs because it might still exist in config files
|
||||
if pn not in default_prefs:
|
||||
continue
|
||||
self.add_checked_input(pn,
|
||||
deluge.ui.console.modes.alltorrents.prefs_to_names[cpn],
|
||||
parent.console_config[pn])
|
||||
self.add_header("Column Widths (-1 = expand)", True)
|
||||
for cpn in deluge.ui.console.modes.alltorrents.column_pref_names:
|
||||
pn = "%s_width" % cpn
|
||||
if pn not in default_prefs:
|
||||
continue
|
||||
self.add_int_spin_input(pn,
|
||||
deluge.ui.console.modes.alltorrents.prefs_to_names[cpn],
|
||||
parent.console_config[pn], -1, 100)
|
||||
|
||||
|
||||
class DownloadsPane(BasePane):
|
||||
def __init__(self, offset, parent, width):
|
||||
BasePane.__init__(self, offset, parent, width)
|
||||
|
||||
self.add_header("Folders")
|
||||
self.add_text_input("download_location", "Download To:", parent.core_config["download_location"])
|
||||
cmptxt = TextInput(self.parent, self.move, self.width, None, "move_completed_path",
|
||||
parent.core_config["move_completed_path"], False)
|
||||
self.add_checkedplus_input("move_completed", "Move completed to:", cmptxt, parent.core_config["move_completed"])
|
||||
copytxt = TextInput(self.parent, self.move, self.width, None, "torrentfiles_location",
|
||||
parent.core_config["torrentfiles_location"], False)
|
||||
self.add_checkedplus_input("copy_torrent_file", "Copy of .torrent files to:", copytxt,
|
||||
parent.core_config["copy_torrent_file"])
|
||||
self.add_checked_input("del_copy_torrent_file", "Delete copy of torrent file on remove",
|
||||
parent.core_config["del_copy_torrent_file"])
|
||||
|
||||
self.add_header("Options", True)
|
||||
self.add_checked_input("prioritize_first_last_pieces", "Prioritize first and last pieces of torrent",
|
||||
parent.core_config["prioritize_first_last_pieces"])
|
||||
self.add_checked_input("sequential_download", "",
|
||||
parent.core_config["sequential_download"])
|
||||
self.add_checked_input("add_paused", "Sequential_download", parent.core_config["add_paused"])
|
||||
self.add_checked_input("pre_allocate_storage", "Pre-Allocate disk space",
|
||||
parent.core_config["pre_allocate_storage"])
|
||||
|
||||
|
||||
class NetworkPane(BasePane):
|
||||
def __init__(self, offset, parent, width):
|
||||
BasePane.__init__(self, offset, parent, width)
|
||||
self.add_header("Incomming Port")
|
||||
inrand = CheckedInput(parent, "Use Random Port Active Port: %d" % parent.active_port,
|
||||
"random_port", parent.core_config["random_port"])
|
||||
self.inputs.append(inrand)
|
||||
listen_ports = parent.core_config["listen_ports"]
|
||||
self.infrom = IntSpinInput(self.parent, " ", "listen_port", self.move, listen_ports[0], 0, 65535)
|
||||
self.infrom.set_depend(inrand, True)
|
||||
self.inputs.append(self.infrom)
|
||||
|
||||
self.add_header("Outgoing Ports", True)
|
||||
outrand = CheckedInput(parent, "Use Random Ports", "random_outgoing_ports",
|
||||
parent.core_config["random_outgoing_ports"])
|
||||
self.inputs.append(outrand)
|
||||
out_ports = parent.core_config["outgoing_ports"]
|
||||
self.outfrom = IntSpinInput(self.parent, " From:", "out_ports_from", self.move, out_ports[0], 0, 65535)
|
||||
self.outfrom.set_depend(outrand, True)
|
||||
self.outto = IntSpinInput(self.parent, " To: ", "out_ports_to", self.move, out_ports[1], 0, 65535)
|
||||
self.outto.set_depend(outrand, True)
|
||||
self.inputs.append(self.outfrom)
|
||||
self.inputs.append(self.outto)
|
||||
|
||||
self.add_header("Interface", True)
|
||||
self.add_text_input("listen_interface", "IP address of the interface to listen on (leave empty for default):",
|
||||
parent.core_config["listen_interface"])
|
||||
|
||||
self.add_header("TOS", True)
|
||||
self.add_text_input("peer_tos", "Peer TOS Byte:", parent.core_config["peer_tos"])
|
||||
|
||||
self.add_header("Network Extras")
|
||||
self.add_checked_input("upnp", "UPnP", parent.core_config["upnp"])
|
||||
self.add_checked_input("natpmp", "NAT-PMP", parent.core_config["natpmp"])
|
||||
self.add_checked_input("utpex", "Peer Exchange", parent.core_config["utpex"])
|
||||
self.add_checked_input("lt_tex", "Tracker Exchange", parent.core_config["lt_tex"])
|
||||
self.add_checked_input("lsd", "LSD", parent.core_config["lsd"])
|
||||
self.add_checked_input("dht", "DHT", parent.core_config["dht"])
|
||||
|
||||
self.add_header("Encryption", True)
|
||||
self.add_select_input("enc_in_policy", "Inbound:", ["Forced", "Enabled", "Disabled"], [0, 1, 2],
|
||||
parent.core_config["enc_in_policy"])
|
||||
self.add_select_input("enc_out_policy", "Outbound:", ["Forced", "Enabled", "Disabled"], [0, 1, 2],
|
||||
parent.core_config["enc_out_policy"])
|
||||
self.add_select_input("enc_level", "Level:", ["Handshake", "Full Stream", "Either"], [0, 1, 2],
|
||||
parent.core_config["enc_level"])
|
||||
|
||||
|
||||
class BandwidthPane(BasePane):
|
||||
def __init__(self, offset, parent, width):
|
||||
BasePane.__init__(self, offset, parent, width)
|
||||
self.add_header("Global Bandwidth Usage")
|
||||
self.add_int_spin_input("max_connections_global", "Maximum Connections:",
|
||||
parent.core_config["max_connections_global"], -1, 9000)
|
||||
self.add_int_spin_input("max_upload_slots_global", "Maximum Upload Slots:",
|
||||
parent.core_config["max_upload_slots_global"], -1, 9000)
|
||||
self.add_float_spin_input("max_download_speed", "Maximum Download Speed (KiB/s):",
|
||||
parent.core_config["max_download_speed"], 1.0, 1, -1.0, 60000.0)
|
||||
self.add_float_spin_input("max_upload_speed", "Maximum Upload Speed (KiB/s):",
|
||||
parent.core_config["max_upload_speed"], 1.0, 1, -1.0, 60000.0)
|
||||
self.add_int_spin_input("max_half_open_connections", "Maximum Half-Open Connections:",
|
||||
parent.core_config["max_half_open_connections"], -1, 9999)
|
||||
self.add_int_spin_input("max_connections_per_second", "Maximum Connection Attempts per Second:",
|
||||
parent.core_config["max_connections_per_second"], -1, 9999)
|
||||
self.add_checked_input("ignore_limits_on_local_network", "Ignore limits on local network",
|
||||
parent.core_config["ignore_limits_on_local_network"])
|
||||
self.add_checked_input("rate_limit_ip_overhead", "Rate Limit IP Overhead",
|
||||
parent.core_config["rate_limit_ip_overhead"])
|
||||
self.add_header("Per Torrent Bandwidth Usage", True)
|
||||
self.add_int_spin_input("max_connections_per_torrent", "Maximum Connections:",
|
||||
parent.core_config["max_connections_per_torrent"], -1, 9000)
|
||||
self.add_int_spin_input("max_upload_slots_per_torrent", "Maximum Upload Slots:",
|
||||
parent.core_config["max_upload_slots_per_torrent"], -1, 9000)
|
||||
self.add_float_spin_input("max_download_speed_per_torrent", "Maximum Download Speed (KiB/s):",
|
||||
parent.core_config["max_download_speed_per_torrent"], 1.0, 1, -1.0, 60000.0)
|
||||
self.add_float_spin_input("max_upload_speed_per_torrent", "Maximum Upload Speed (KiB/s):",
|
||||
parent.core_config["max_upload_speed_per_torrent"], 1.0, 1, -1.0, 60000.0)
|
||||
|
||||
|
||||
class OtherPane(BasePane):
|
||||
def __init__(self, offset, parent, width):
|
||||
BasePane.__init__(self, offset, parent, width)
|
||||
self.add_header("System Information")
|
||||
self.add_info_field(" Help us improve Deluge by sending us your", "", "")
|
||||
self.add_info_field(" Python version, PyGTK version, OS and processor", "", "")
|
||||
self.add_info_field(" types. Absolutely no other information is sent.", "", "")
|
||||
self.add_checked_input("send_info", "Yes, please send anonymous statistics.", parent.core_config["send_info"])
|
||||
self.add_header("GeoIP Database", True)
|
||||
self.add_text_input("geoip_db_location", "Location:", parent.core_config["geoip_db_location"])
|
||||
|
||||
|
||||
class DaemonPane(BasePane):
|
||||
def __init__(self, offset, parent, width):
|
||||
BasePane.__init__(self, offset, parent, width)
|
||||
self.add_header("Port")
|
||||
self.add_int_spin_input("daemon_port", "Daemon Port:", parent.core_config["daemon_port"], 0, 65535)
|
||||
self.add_header("Connections", True)
|
||||
self.add_checked_input("allow_remote", "Allow remote connections", parent.core_config["allow_remote"])
|
||||
self.add_header("Other", True)
|
||||
self.add_checked_input("new_release_check", "Periodically check the website for new releases",
|
||||
parent.core_config["new_release_check"])
|
||||
|
||||
|
||||
class QueuePane(BasePane):
|
||||
def __init__(self, offset, parent, width):
|
||||
BasePane.__init__(self, offset, parent, width)
|
||||
self.add_header("New Torrents")
|
||||
self.add_checked_input("queue_new_to_top", "Queue to top", parent.core_config["queue_new_to_top"])
|
||||
self.add_header("Active Torrents", True)
|
||||
self.add_int_spin_input("max_active_limit", "Total:", parent.core_config["max_active_limit"], -1, 9999)
|
||||
self.add_int_spin_input("max_active_downloading", "Downloading:",
|
||||
parent.core_config["max_active_downloading"], -1, 9999)
|
||||
self.add_int_spin_input("max_active_seeding", "Seeding:",
|
||||
parent.core_config["max_active_seeding"], -1, 9999)
|
||||
self.add_checked_input("dont_count_slow_torrents", "Ignore slow torrents",
|
||||
parent.core_config["dont_count_slow_torrents"])
|
||||
self.add_checked_input("auto_manage_prefer_seeds", "Prefer seeding torrents",
|
||||
parent.core_config["auto_manage_prefer_seeds"])
|
||||
self.add_header("Seeding Rotation", True)
|
||||
self.add_float_spin_input("share_ratio_limit", "Share Ratio:",
|
||||
parent.core_config["share_ratio_limit"], 1.0, 2, -1.0, 100.0)
|
||||
self.add_float_spin_input("seed_time_ratio_limit", "Time Ratio:",
|
||||
parent.core_config["seed_time_ratio_limit"], 1.0, 2, -1.0, 100.0)
|
||||
self.add_int_spin_input("seed_time_limit", "Time (m):", parent.core_config["seed_time_limit"], -1, 10000)
|
||||
seedratio = FloatSpinInput(self.parent, "", "stop_seed_ratio", self.move,
|
||||
parent.core_config["stop_seed_ratio"], 0.1, 2, 0.5, 100.0)
|
||||
self.add_checkedplus_input("stop_seed_at_ratio", "Share Ratio Reached:", seedratio,
|
||||
parent.core_config["stop_seed_at_ratio"])
|
||||
self.add_checked_input("remove_seed_at_ratio", "Remove torrent (Unchecked pauses torrent)",
|
||||
parent.core_config["remove_seed_at_ratio"])
|
||||
|
||||
|
||||
class ProxyPane(BasePane):
|
||||
def __init__(self, offset, parent, width):
|
||||
BasePane.__init__(self, offset, parent, width)
|
||||
self.add_header("Proxy Settings")
|
||||
self.add_header("Proxy", True)
|
||||
proxy = parent.core_config["proxy"]
|
||||
self.add_int_spin_input("proxy_type", "Type:", proxy["type"], 0, 5)
|
||||
self.add_info_field(" 0: None 1: Socks4 2: Socks5", "", "")
|
||||
self.add_info_field(" 3: Socks5 Auth 4: HTTP 5: HTTP Auth", "", "")
|
||||
self.add_text_input("proxy_username", "Username:", proxy["username"])
|
||||
self.add_text_input("proxy_password", "Password:", proxy["password"])
|
||||
self.add_text_input("proxy_hostname", "Hostname:", proxy["hostname"])
|
||||
self.add_int_spin_input("proxy_port", "Port:", proxy["port"], 0, 65535)
|
||||
self.add_checked_input("proxy_hostnames", "Proxy hostnames", proxy["proxy_hostnames"])
|
||||
self.add_checked_input("proxy_peer_connections", "Proxy peer connections", proxy["proxy_peer_connections"])
|
||||
|
||||
self.add_header("I2P Proxy", True)
|
||||
i2p_proxy = parent.core_config["i2p_proxy"]
|
||||
self.add_text_input("i2p_hostname", "Hostname:", i2p_proxy["hostname"])
|
||||
self.add_int_spin_input("i2p_port", "Port:", i2p_proxy["port"], 0, 65535)
|
||||
self.add_checked_input("anonymous_mode", "Anonymous Mode", parent.core_config["anonymous_mode"])
|
||||
|
||||
|
||||
class CachePane(BasePane):
|
||||
def __init__(self, offset, parent, width):
|
||||
BasePane.__init__(self, offset, parent, width)
|
||||
self.add_header("Settings")
|
||||
self.add_int_spin_input("cache_size", "Cache Size (16 KiB blocks):", parent.core_config["cache_size"], 0, 99999)
|
||||
self.add_int_spin_input("cache_expiry", "Cache Expiry (seconds):", parent.core_config["cache_expiry"], 1, 32000)
|
||||
self.add_header("Status (press 'r' to refresh status)", True)
|
||||
self.add_header(" Write")
|
||||
self.add_info_field(" Blocks Written:", self.parent.status["blocks_written"], "blocks_written")
|
||||
self.add_info_field(" Writes:", self.parent.status["writes"], "writes")
|
||||
self.add_info_field(" Write Cache Hit Ratio:", "%.2f" % self.parent.status["write_hit_ratio"],
|
||||
"write_hit_ratio")
|
||||
self.add_header(" Read")
|
||||
self.add_info_field(" Blocks Read:", self.parent.status["blocks_read"], "blocks_read")
|
||||
self.add_info_field(" Blocks Read hit:", self.parent.status["blocks_read_hit"], "blocks_read_hit")
|
||||
self.add_info_field(" Reads:", self.parent.status["reads"], "reads")
|
||||
self.add_info_field(" Read Cache Hit Ratio:", "%.2f" % self.parent.status["read_hit_ratio"], "read_hit_ratio")
|
||||
self.add_header(" Size")
|
||||
self.add_info_field(" Cache Size:", self.parent.status["cache_size"], "cache_size")
|
||||
self.add_info_field(" Read Cache Size:", self.parent.status["read_cache_size"], "read_cache_size")
|
||||
|
||||
def update_cache_status(self, status):
|
||||
for ipt in self.inputs:
|
||||
if isinstance(ipt, InfoField):
|
||||
try:
|
||||
ipt.set_value(status[ipt.name])
|
||||
except KeyError:
|
||||
pass
|
@ -1,296 +0,0 @@
|
||||
# -*- 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.
|
||||
#
|
||||
|
||||
import logging
|
||||
from collections import deque
|
||||
|
||||
import deluge.component as component
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console.modes.basemode import BaseMode
|
||||
from deluge.ui.console.modes.input_popup import Popup, SelectInput
|
||||
from deluge.ui.console.modes.popup import MessagePopup
|
||||
from deluge.ui.console.modes.preference_panes import (BandwidthPane, CachePane, ColumnsPane, DaemonPane, DownloadsPane,
|
||||
InterfacePane, NetworkPane, OtherPane, ProxyPane, QueuePane)
|
||||
|
||||
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 lets you view and configure various options in deluge.
|
||||
|
||||
There are three main sections to this screen. Only one
|
||||
section is active at a time. You can switch the active
|
||||
section by hitting TAB (or Shift-TAB to go back one)
|
||||
|
||||
The section on the left displays the various categories
|
||||
that the settings fall in. You can navigate the list
|
||||
using the up/down arrows
|
||||
|
||||
The section on the right shows the settings for the
|
||||
selected category. When this section is active
|
||||
you can navigate the various settings with the up/down
|
||||
arrows. Special keys for each input type are described
|
||||
below.
|
||||
|
||||
The final section is at the bottom right, the:
|
||||
[Cancel] [Apply] [OK] buttons. When this section
|
||||
is active, simply select the option you want using
|
||||
the arrow keys and press Enter to confim.
|
||||
|
||||
|
||||
Special keys for various input types are as follows:
|
||||
- For text inputs you can simply type in the value.
|
||||
|
||||
- For numeric inputs (indicated by the value being
|
||||
in []s), you can type a value, or use PageUp and
|
||||
PageDown to increment/decrement the value.
|
||||
|
||||
- For checkbox inputs use the spacebar to toggle
|
||||
|
||||
- For checkbox plus something else inputs (the
|
||||
something else being only visible when you
|
||||
check the box) you can toggle the check with
|
||||
space, use the right arrow to edit the other
|
||||
value, and escape to get back to the check box.
|
||||
|
||||
|
||||
"""
|
||||
HELP_LINES = HELP_STR.split("\n")
|
||||
|
||||
|
||||
class ZONE(object):
|
||||
CATEGORIES = 0
|
||||
PREFRENCES = 1
|
||||
ACTIONS = 2
|
||||
|
||||
|
||||
class Preferences(BaseMode):
|
||||
def __init__(self, parent_mode, core_config, console_config, active_port, status, stdscr, encoding=None):
|
||||
self.parent_mode = parent_mode
|
||||
self.categories = [_("Interface"), _("Columns"), _("Downloads"), _("Network"), _("Bandwidth"),
|
||||
_("Other"), _("Daemon"), _("Queue"), _("Proxy"), _("Cache")] # , _("Plugins")]
|
||||
self.cur_cat = 0
|
||||
self.popup = None
|
||||
self.messages = deque()
|
||||
self.action_input = None
|
||||
|
||||
self.core_config = core_config
|
||||
self.console_config = console_config
|
||||
self.active_port = active_port
|
||||
self.status = status
|
||||
|
||||
self.active_zone = ZONE.CATEGORIES
|
||||
|
||||
# how wide is the left 'pane' with categories
|
||||
self.div_off = 15
|
||||
|
||||
BaseMode.__init__(self, stdscr, encoding, False)
|
||||
|
||||
# create the panes
|
||||
self.__calc_sizes()
|
||||
|
||||
self.action_input = SelectInput(self, None, None, ["Cancel", "Apply", "OK"], [0, 1, 2], 0)
|
||||
self.refresh()
|
||||
|
||||
def __calc_sizes(self):
|
||||
self.prefs_width = self.cols - self.div_off - 1
|
||||
self.prefs_height = self.rows - 4
|
||||
# Needs to be same order as self.categories
|
||||
self.panes = [
|
||||
InterfacePane(self.div_off + 2, self, self.prefs_width),
|
||||
ColumnsPane(self.div_off + 2, self, self.prefs_width),
|
||||
DownloadsPane(self.div_off + 2, self, self.prefs_width),
|
||||
NetworkPane(self.div_off + 2, self, self.prefs_width),
|
||||
BandwidthPane(self.div_off + 2, self, self.prefs_width),
|
||||
OtherPane(self.div_off + 2, self, self.prefs_width),
|
||||
DaemonPane(self.div_off + 2, self, self.prefs_width),
|
||||
QueuePane(self.div_off + 2, self, self.prefs_width),
|
||||
ProxyPane(self.div_off + 2, self, self.prefs_width),
|
||||
CachePane(self.div_off + 2, self, self.prefs_width)
|
||||
]
|
||||
|
||||
def __draw_catetories(self):
|
||||
for i, category in enumerate(self.categories):
|
||||
if i == self.cur_cat and self.active_zone == ZONE.CATEGORIES:
|
||||
self.add_string(i + 1, "- {!black,white,bold!}%s" % category, pad=False)
|
||||
elif i == self.cur_cat:
|
||||
self.add_string(i + 1, "- {!black,white!}%s" % category, pad=False)
|
||||
else:
|
||||
self.add_string(i + 1, "- %s" % category)
|
||||
self.stdscr.vline(1, self.div_off, "|", self.rows - 2)
|
||||
|
||||
def __draw_preferences(self):
|
||||
self.panes[self.cur_cat].render(self, self.stdscr, self.prefs_width, self.active_zone == ZONE.PREFRENCES)
|
||||
|
||||
def __draw_actions(self):
|
||||
selected = self.active_zone == ZONE.ACTIONS
|
||||
self.stdscr.hline(self.rows - 3, self.div_off + 1, "_", self.cols)
|
||||
self.action_input.render(self.stdscr, self.rows - 2, self.cols, selected, self.cols - 22)
|
||||
|
||||
def on_resize(self, *args):
|
||||
BaseMode.on_resize_norefresh(self, *args)
|
||||
self.__calc_sizes()
|
||||
|
||||
# Always refresh Legacy(it will also refresh AllTorrents), otherwise it will bug deluge out
|
||||
legacy = component.get("LegacyUI")
|
||||
legacy.on_resize(*args)
|
||||
self.stdscr.erase()
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
if self.popup is None and self.messages:
|
||||
title, msg = self.messages.popleft()
|
||||
self.popup = MessagePopup(self, title, msg)
|
||||
|
||||
self.stdscr.erase()
|
||||
self.add_string(0, self.statusbars.topbar)
|
||||
hstr = "%sPress [h] for help" % (" " * (self.cols - len(self.statusbars.bottombar) - 10))
|
||||
self.add_string(self.rows - 1, "%s%s" % (self.statusbars.bottombar, hstr))
|
||||
|
||||
self.__draw_catetories()
|
||||
self.__draw_actions()
|
||||
|
||||
# do this last since it moves the cursor
|
||||
self.__draw_preferences()
|
||||
|
||||
if component.get("ConsoleUI").screen != self:
|
||||
return
|
||||
|
||||
self.stdscr.noutrefresh()
|
||||
|
||||
if self.popup:
|
||||
self.popup.refresh()
|
||||
|
||||
curses.doupdate()
|
||||
|
||||
def __category_read(self, c):
|
||||
# Navigate prefs
|
||||
if c == curses.KEY_UP:
|
||||
self.cur_cat = max(0, self.cur_cat - 1)
|
||||
elif c == curses.KEY_DOWN:
|
||||
self.cur_cat = min(len(self.categories) - 1, self.cur_cat + 1)
|
||||
|
||||
def __prefs_read(self, c):
|
||||
self.panes[self.cur_cat].handle_read(c)
|
||||
|
||||
def __apply_prefs(self):
|
||||
new_core_config = {}
|
||||
for pane in self.panes:
|
||||
if not isinstance(pane, InterfacePane) and not isinstance(pane, ColumnsPane):
|
||||
pane.add_config_values(new_core_config)
|
||||
# Apply Core Prefs
|
||||
if client.connected():
|
||||
# Only do this if we're connected to a daemon
|
||||
config_to_set = {}
|
||||
for key in new_core_config.keys():
|
||||
# The values do not match so this needs to be updated
|
||||
if self.core_config[key] != new_core_config[key]:
|
||||
config_to_set[key] = new_core_config[key]
|
||||
|
||||
if config_to_set:
|
||||
# Set each changed config value in the core
|
||||
client.core.set_config(config_to_set)
|
||||
client.force_call(True)
|
||||
# Update the configuration
|
||||
self.core_config.update(config_to_set)
|
||||
|
||||
# Update Interface Prefs
|
||||
new_console_config = {}
|
||||
didupdate = False
|
||||
for pane in self.panes:
|
||||
# could just access panes by index, but that would break if panes
|
||||
# are ever reordered, so do it the slightly slower but safer way
|
||||
if isinstance(pane, InterfacePane) or isinstance(pane, ColumnsPane):
|
||||
pane.add_config_values(new_console_config)
|
||||
for key in new_console_config.keys():
|
||||
# The values do not match so this needs to be updated
|
||||
if self.console_config[key] != new_console_config[key]:
|
||||
self.console_config[key] = new_console_config[key]
|
||||
didupdate = True
|
||||
if didupdate:
|
||||
# changed something, save config and tell alltorrents
|
||||
self.console_config.save()
|
||||
self.parent_mode.update_config()
|
||||
|
||||
def __update_preferences(self, core_config):
|
||||
self.core_config = core_config
|
||||
for pane in self.panes:
|
||||
pane.update_values(core_config)
|
||||
|
||||
def __actions_read(self, c):
|
||||
self.action_input.handle_read(c)
|
||||
if c == curses.KEY_ENTER or c == 10:
|
||||
# take action
|
||||
if self.action_input.selidx == 0: # Cancel
|
||||
self.back_to_parent()
|
||||
elif self.action_input.selidx == 1: # Apply
|
||||
self.__apply_prefs()
|
||||
client.core.get_config().addCallback(self.__update_preferences)
|
||||
elif self.action_input.selidx == 2: # OK
|
||||
self.__apply_prefs()
|
||||
self.back_to_parent()
|
||||
|
||||
def back_to_parent(self):
|
||||
self.stdscr.erase()
|
||||
component.get("ConsoleUI").set_mode(self.parent_mode)
|
||||
self.parent_mode.resume()
|
||||
|
||||
def read_input(self):
|
||||
c = self.stdscr.getch()
|
||||
|
||||
if self.popup:
|
||||
if self.popup.handle_read(c):
|
||||
self.popup = None
|
||||
self.refresh()
|
||||
return
|
||||
|
||||
if c > 31 and c < 256:
|
||||
if chr(c) == "Q":
|
||||
from twisted.internet import reactor
|
||||
if client.connected():
|
||||
def on_disconnect(result):
|
||||
reactor.stop()
|
||||
client.disconnect().addCallback(on_disconnect)
|
||||
else:
|
||||
reactor.stop()
|
||||
return
|
||||
elif chr(c) == "h":
|
||||
self.popup = Popup(self, "Preferences Help")
|
||||
for l in HELP_LINES:
|
||||
self.popup.add_line(l)
|
||||
|
||||
if c == 9:
|
||||
self.active_zone += 1
|
||||
if self.active_zone > ZONE.ACTIONS:
|
||||
self.active_zone = ZONE.CATEGORIES
|
||||
elif c == 27 and self.active_zone == ZONE.CATEGORIES:
|
||||
self.back_to_parent()
|
||||
elif c == curses.KEY_BTAB:
|
||||
self.active_zone -= 1
|
||||
if self.active_zone < ZONE.CATEGORIES:
|
||||
self.active_zone = ZONE.ACTIONS
|
||||
|
||||
elif c == 114 and isinstance(self.panes[self.cur_cat], CachePane):
|
||||
client.core.get_cache_status().addCallback(self.panes[self.cur_cat].update_cache_status)
|
||||
|
||||
else:
|
||||
if self.active_zone == ZONE.CATEGORIES:
|
||||
self.__category_read(c)
|
||||
elif self.active_zone == ZONE.PREFRENCES:
|
||||
self.__prefs_read(c)
|
||||
elif self.active_zone == ZONE.ACTIONS:
|
||||
self.__actions_read(c)
|
||||
|
||||
self.refresh()
|
1
deluge/ui/console/modes/preferences/__init__.py
Normal file
1
deluge/ui/console/modes/preferences/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from deluge.ui.console.modes.preferences.preferences import Preferences # NOQA
|
447
deluge/ui/console/modes/preferences/preference_panes.py
Normal file
447
deluge/ui/console/modes/preferences/preference_panes.py
Normal file
@ -0,0 +1,447 @@
|
||||
# -*- 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.
|
||||
#
|
||||
|
||||
import logging
|
||||
|
||||
from deluge.common import is_ip
|
||||
from deluge.decorators import overrides
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console.widgets import BaseInputPane, BaseWindow
|
||||
from deluge.ui.console.widgets.fields import FloatSpinInput, TextInput
|
||||
from deluge.ui.console.widgets.popup import PopupsHandler
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BasePreferencePane(BaseInputPane, BaseWindow, PopupsHandler):
|
||||
|
||||
def __init__(self, name, preferences):
|
||||
PopupsHandler.__init__(self)
|
||||
self.preferences = preferences
|
||||
BaseWindow.__init__(self, "%s" % name, self.pane_width, preferences.height, posy=1, posx=self.pane_x_pos)
|
||||
BaseInputPane.__init__(self, preferences, border_off_east=1)
|
||||
self.name = name
|
||||
|
||||
# have we scrolled down in the list
|
||||
self.input_offset = 0
|
||||
|
||||
@overrides(BaseInputPane)
|
||||
def handle_read(self, c):
|
||||
if self.popup:
|
||||
ret = self.popup.handle_read(c)
|
||||
if self.popup and self.popup.closed():
|
||||
self.pop_popup()
|
||||
self.refresh()
|
||||
return ret
|
||||
return BaseInputPane.handle_read(self, c)
|
||||
|
||||
@property
|
||||
def visible_content_pane_height(self):
|
||||
y, x = self.visible_content_pane_size
|
||||
return y
|
||||
|
||||
@property
|
||||
def pane_x_pos(self):
|
||||
return self.preferences.sidebar_width
|
||||
|
||||
@property
|
||||
def pane_width(self):
|
||||
return self.preferences.width
|
||||
|
||||
@property
|
||||
def cols(self):
|
||||
return self.pane_width
|
||||
|
||||
@property
|
||||
def rows(self):
|
||||
return self.preferences.height
|
||||
|
||||
def is_active_pane(self):
|
||||
return self.preferences.is_active_pane(self)
|
||||
|
||||
def create_pane(self, core_conf, console_config):
|
||||
pass
|
||||
|
||||
def add_config_values(self, conf_dict):
|
||||
for ipt in self.inputs:
|
||||
if ipt.has_input():
|
||||
# gross, have to special case in/out ports since they are tuples
|
||||
if ipt.name in ("listen_ports_to", "listen_ports_from", "out_ports_from", "out_ports_to",
|
||||
"i2p_port", "i2p_hostname", "proxy_type", "proxy_username", "proxy_hostnames",
|
||||
"proxy_password", "proxy_hostname", "proxy_port", "proxy_peer_connections",
|
||||
"listen_interface"):
|
||||
if ipt.name == "listen_ports_to":
|
||||
conf_dict["listen_ports"] = (self.infrom.get_value(), self.into.get_value())
|
||||
elif ipt.name == "out_ports_to":
|
||||
conf_dict["outgoing_ports"] = (self.outfrom.get_value(), self.outto.get_value())
|
||||
elif ipt.name == "listen_interface":
|
||||
interface = ipt.get_value().strip()
|
||||
if is_ip(interface) or not interface:
|
||||
conf_dict["listen_interface"] = interface
|
||||
elif ipt.name == "i2p_port":
|
||||
conf_dict.setdefault("i2p_proxy", {})["port"] = ipt.get_value()
|
||||
elif ipt.name == "i2p_hostname":
|
||||
conf_dict.setdefault("i2p_proxy", {})["hostname"] = ipt.get_value()
|
||||
elif ipt.name == "proxy_type":
|
||||
conf_dict.setdefault("proxy", {})["type"] = ipt.get_value()
|
||||
elif ipt.name == "proxy_username":
|
||||
conf_dict.setdefault("proxy", {})["username"] = ipt.get_value()
|
||||
elif ipt.name == "proxy_password":
|
||||
conf_dict.setdefault("proxy", {})["password"] = ipt.get_value()
|
||||
elif ipt.name == "proxy_hostname":
|
||||
conf_dict.setdefault("proxy", {})["hostname"] = ipt.get_value()
|
||||
elif ipt.name == "proxy_port":
|
||||
conf_dict.setdefault("proxy", {})["port"] = ipt.get_value()
|
||||
elif ipt.name == "proxy_hostnames":
|
||||
conf_dict.setdefault("proxy", {})["proxy_hostnames"] = ipt.get_value()
|
||||
elif ipt.name == "proxy_peer_connections":
|
||||
conf_dict.setdefault("proxy", {})["proxy_peer_connections"] = ipt.get_value()
|
||||
else:
|
||||
conf_dict[ipt.name] = ipt.get_value()
|
||||
if hasattr(ipt, "get_child"):
|
||||
c = ipt.get_child()
|
||||
conf_dict[c.name] = c.get_value()
|
||||
|
||||
def update_values(self, conf_dict):
|
||||
for ipt in self.inputs:
|
||||
if ipt.has_input():
|
||||
try:
|
||||
ipt.set_value(conf_dict[ipt.name])
|
||||
except KeyError: # just ignore if it's not in dict
|
||||
pass
|
||||
if hasattr(ipt, "get_child"):
|
||||
try:
|
||||
c = ipt.get_child()
|
||||
c.set_value(conf_dict[c.name])
|
||||
except KeyError: # just ignore if it's not in dict
|
||||
pass
|
||||
|
||||
def render(self, mode, screen, width, focused):
|
||||
height = self.get_content_height()
|
||||
self.ensure_content_pane_height(height)
|
||||
self.screen.erase()
|
||||
|
||||
if focused and self.active_input == -1:
|
||||
self.move_active_down(1)
|
||||
|
||||
self.render_inputs(focused=focused)
|
||||
|
||||
@overrides(BaseWindow)
|
||||
def refresh(self):
|
||||
BaseWindow.refresh(self)
|
||||
if self.popup:
|
||||
self.popup.refresh()
|
||||
|
||||
def update(self, active):
|
||||
pass
|
||||
|
||||
|
||||
class InterfacePane(BasePreferencePane):
|
||||
|
||||
def __init__(self, preferences):
|
||||
BasePreferencePane.__init__(self, " %s " % _("Interface"), preferences)
|
||||
|
||||
@overrides(BasePreferencePane)
|
||||
def create_pane(self, core_conf, console_config):
|
||||
self.add_header(_("General options"))
|
||||
|
||||
self.add_checked_input("ring_bell", _("Ring system bell when a download finishes"),
|
||||
console_config["ring_bell"])
|
||||
self.add_header("Console UI", space_above=True)
|
||||
self.add_checked_input("separate_complete",
|
||||
_("List complete torrents after incomplete regardless of sorting order"),
|
||||
console_config["torrentview"]["separate_complete"])
|
||||
self.add_checked_input("move_selection", _("Move selection when moving torrents in the queue"),
|
||||
console_config["torrentview"]["move_selection"])
|
||||
from deluge.ui.util import lang
|
||||
langs = lang.get_languages()
|
||||
langs.insert(0, ("", "System Default"))
|
||||
self.add_combo_input("language", _("Language"),
|
||||
langs, default=console_config["language"])
|
||||
self.add_header(_("Command Line Mode"), space_above=True)
|
||||
self.add_checked_input("ignore_duplicate_lines", _("Do not store duplicate input in history"),
|
||||
console_config["cmdline"]["ignore_duplicate_lines"])
|
||||
self.add_checked_input("save_command_history", _("Store and load command line history in command line mode"),
|
||||
console_config["cmdline"]["save_command_history"])
|
||||
self.add_header("")
|
||||
self.add_checked_input("third_tab_lists_all", _("Third tab lists all remaining torrents in command line mode"),
|
||||
console_config["cmdline"]["third_tab_lists_all"])
|
||||
self.add_int_spin_input("torrents_per_tab_press", _("Torrents per tab press"),
|
||||
console_config["cmdline"]["torrents_per_tab_press"], min_val=5, max_val=10000)
|
||||
|
||||
|
||||
class DownloadsPane(BasePreferencePane):
|
||||
|
||||
def __init__(self, preferences):
|
||||
BasePreferencePane.__init__(self, " %s " % _("Downloads"), preferences)
|
||||
|
||||
@overrides(BasePreferencePane)
|
||||
def create_pane(self, core_conf, console_config):
|
||||
self.add_header(_("Folders"))
|
||||
self.add_text_input("download_location", "%s:" % _("Download To"), core_conf["download_location"],
|
||||
complete=True, activate_input=True, col="+1")
|
||||
cmptxt = TextInput(self.preferences, "move_completed_path", None, self.move, self.pane_width,
|
||||
core_conf["move_completed_path"], False)
|
||||
self.add_checkedplus_input("move_completed", "%s:" % _("Move completed to"),
|
||||
cmptxt, core_conf["move_completed"])
|
||||
copytxt = TextInput(self.preferences, "torrentfiles_location", None, self.move, self.pane_width,
|
||||
core_conf["torrentfiles_location"], False)
|
||||
self.add_checkedplus_input("copy_torrent_file", "%s:" % _("Copy of .torrent files to"), copytxt,
|
||||
core_conf["copy_torrent_file"])
|
||||
self.add_checked_input("del_copy_torrent_file", _("Delete copy of torrent file on remove"),
|
||||
core_conf["del_copy_torrent_file"])
|
||||
|
||||
self.add_header(_("Options"), space_above=True)
|
||||
self.add_checked_input("prioritize_first_last_pieces", ("Prioritize first and last pieces of torrent"),
|
||||
core_conf["prioritize_first_last_pieces"])
|
||||
self.add_checked_input("sequential_download", _("Sequential download"),
|
||||
core_conf["sequential_download"])
|
||||
self.add_checked_input("add_paused", _("Add Paused"), core_conf["add_paused"])
|
||||
self.add_checked_input("pre_allocate_storage", _("Pre-Allocate disk space"),
|
||||
core_conf["pre_allocate_storage"])
|
||||
|
||||
|
||||
class NetworkPane(BasePreferencePane):
|
||||
|
||||
def __init__(self, preferences):
|
||||
BasePreferencePane.__init__(self, " %s " % _("Network"), preferences)
|
||||
|
||||
@overrides(BasePreferencePane)
|
||||
def create_pane(self, core_conf, console_config):
|
||||
self.add_header(_("Incomming Ports"))
|
||||
inrand = self.add_checked_input("random_port", "Use Random Ports Active Port: %d"
|
||||
% self.preferences.active_port,
|
||||
core_conf["random_port"])
|
||||
listen_ports = core_conf["listen_ports"]
|
||||
self.infrom = self.add_int_spin_input("listen_ports_from", " %s:" % _("From"),
|
||||
value=listen_ports[0], min_val=0, max_val=65535)
|
||||
self.infrom.set_depend(inrand, inverse=True)
|
||||
self.into = self.add_int_spin_input("listen_ports_to", " %s:" % _("To"),
|
||||
value=listen_ports[1], min_val=0, max_val=65535)
|
||||
self.into.set_depend(inrand, inverse=True)
|
||||
|
||||
self.add_header(_("Outgoing Ports"), space_above=True)
|
||||
outrand = self.add_checked_input("random_outgoing_ports", _("Use Random Ports"),
|
||||
core_conf["random_outgoing_ports"])
|
||||
out_ports = core_conf["outgoing_ports"]
|
||||
self.outfrom = self.add_int_spin_input("out_ports_from", " %s:" % _("From"),
|
||||
value=out_ports[0], min_val=0, max_val=65535)
|
||||
self.outfrom.set_depend(outrand, inverse=True)
|
||||
self.outto = self.add_int_spin_input("out_ports_to", " %s:" % _("To"),
|
||||
value=out_ports[1], min_val=0, max_val=65535)
|
||||
self.outto.set_depend(outrand, inverse=True)
|
||||
|
||||
self.add_header(_("Interface"), space_above=True)
|
||||
self.add_text_input("listen_interface", "%s:" % _("IP address of the interface to listen on "
|
||||
"(leave empty for default)"),
|
||||
core_conf["listen_interface"])
|
||||
|
||||
self.add_header("TOS", space_above=True)
|
||||
self.add_text_input("peer_tos", "Peer TOS Byte:", core_conf["peer_tos"])
|
||||
|
||||
self.add_header(_("Network Extras"), space_above=True)
|
||||
self.add_checked_input("upnp", "UPnP", core_conf["upnp"])
|
||||
self.add_checked_input("natpmp", "NAT-PMP", core_conf["natpmp"])
|
||||
self.add_checked_input("utpex", "Peer Exchange", core_conf["utpex"])
|
||||
self.add_checked_input("lt_tex", "Tracker Exchange", core_conf["lt_tex"])
|
||||
self.add_checked_input("lsd", "LSD", core_conf["lsd"])
|
||||
self.add_checked_input("dht", "DHT", core_conf["dht"])
|
||||
|
||||
self.add_header(_("Encryption"), space_above=True)
|
||||
self.add_select_input("enc_in_policy", "%s:" % _("Inbound"), [_("Forced"), _("Enabled"), _("Disabled")],
|
||||
[0, 1, 2], core_conf["enc_in_policy"], active_default=True, col="+1")
|
||||
self.add_select_input("enc_out_policy", "%s:" % _("Outbound"), [_("Forced"), _("Enabled"), _("Disabled")],
|
||||
[0, 1, 2], core_conf["enc_out_policy"], active_default=True)
|
||||
self.add_select_input("enc_level", "%s:" % _("Level"), [_("Handshake"), _("Full Stream"), _("Either")],
|
||||
[0, 1, 2], core_conf["enc_level"], active_default=True)
|
||||
|
||||
|
||||
class BandwidthPane(BasePreferencePane):
|
||||
|
||||
def __init__(self, preferences):
|
||||
BasePreferencePane.__init__(self, " %s " % _("Bandwidth"), preferences)
|
||||
|
||||
@overrides(BasePreferencePane)
|
||||
def create_pane(self, core_conf, console_config):
|
||||
self.add_header(_("Global Bandwidth Usage"))
|
||||
self.add_int_spin_input("max_connections_global", "%s:" % _("Maximum Connections"),
|
||||
core_conf["max_connections_global"], min_val=-1, max_val=9000)
|
||||
self.add_int_spin_input("max_upload_slots_global", "%s:" % _("Maximum Upload Slots"),
|
||||
core_conf["max_upload_slots_global"], min_val=-1, max_val=9000)
|
||||
self.add_float_spin_input("max_download_speed", "%s:" % _("Maximum Download Speed (KiB/s)"),
|
||||
core_conf["max_download_speed"], min_val=-1.0, max_val=60000.0)
|
||||
self.add_float_spin_input("max_upload_speed", "%s:" % _("Maximum Upload Speed (KiB/s)"),
|
||||
core_conf["max_upload_speed"], min_val=-1.0, max_val=60000.0)
|
||||
self.add_int_spin_input("max_half_open_connections", "%s:" % _("Maximum Half-Open Connections"),
|
||||
core_conf["max_half_open_connections"], min_val=-1, max_val=9999)
|
||||
self.add_int_spin_input("max_connections_per_second", "%s:" % _("Maximum Connection Attempts per Second"),
|
||||
core_conf["max_connections_per_second"], min_val=-1, max_val=9999)
|
||||
self.add_checked_input("ignore_limits_on_local_network", _("Ignore limits on local network"),
|
||||
core_conf["ignore_limits_on_local_network"])
|
||||
self.add_checked_input("rate_limit_ip_overhead", _("Rate Limit IP Overhead"),
|
||||
core_conf["rate_limit_ip_overhead"])
|
||||
self.add_header(_("Per Torrent Bandwidth Usage"), space_above=True)
|
||||
self.add_int_spin_input("max_connections_per_torrent", "%s:" % _("Maximum Connections"),
|
||||
core_conf["max_connections_per_torrent"], min_val=-1, max_val=9000)
|
||||
self.add_int_spin_input("max_upload_slots_per_torrent", "%s:" % _("Maximum Upload Slots"),
|
||||
core_conf["max_upload_slots_per_torrent"], min_val=-1, max_val=9000)
|
||||
self.add_float_spin_input("max_download_speed_per_torrent", "%s:" % _("Maximum Download Speed (KiB/s)"),
|
||||
core_conf["max_download_speed_per_torrent"], min_val=-1.0, max_val=60000.0)
|
||||
self.add_float_spin_input("max_upload_speed_per_torrent", "%s:" % _("Maximum Upload Speed (KiB/s)"),
|
||||
core_conf["max_upload_speed_per_torrent"], min_val=-1.0, max_val=60000.0)
|
||||
|
||||
|
||||
class OtherPane(BasePreferencePane):
|
||||
|
||||
def __init__(self, preferences):
|
||||
BasePreferencePane.__init__(self, " %s " % _("Other"), preferences)
|
||||
|
||||
@overrides(BasePreferencePane)
|
||||
def create_pane(self, core_conf, console_config):
|
||||
self.add_header(_("System Information"))
|
||||
self.add_info_field("info1", " Help us improve Deluge by sending us your", "")
|
||||
self.add_info_field("info2", " Python version, PyGTK version, OS and processor", "")
|
||||
self.add_info_field("info3", " types. Absolutely no other information is sent.", "")
|
||||
self.add_checked_input("send_info", _("Yes, please send anonymous statistics."), core_conf["send_info"])
|
||||
self.add_header(_("GeoIP Database"), space_above=True)
|
||||
self.add_text_input("geoip_db_location", "Location:", core_conf["geoip_db_location"])
|
||||
|
||||
|
||||
class DaemonPane(BasePreferencePane):
|
||||
|
||||
def __init__(self, preferences):
|
||||
BasePreferencePane.__init__(self, " %s " % _("Daemon"), preferences)
|
||||
|
||||
@overrides(BasePreferencePane)
|
||||
def create_pane(self, core_conf, console_config):
|
||||
self.add_header("Port")
|
||||
self.add_int_spin_input("daemon_port", "%s:" % _("Daemon Port"), core_conf["daemon_port"],
|
||||
min_val=0, max_val=65535)
|
||||
self.add_header("Connections", space_above=True)
|
||||
self.add_checked_input("allow_remote", _("Allow remote connections"), core_conf["allow_remote"])
|
||||
self.add_header("Other", space_above=True)
|
||||
self.add_checked_input("new_release_check", _("Periodically check the website for new releases"),
|
||||
core_conf["new_release_check"])
|
||||
|
||||
|
||||
class QueuePane(BasePreferencePane):
|
||||
|
||||
def __init__(self, preferences):
|
||||
BasePreferencePane.__init__(self, " %s " % _("Queue"), preferences)
|
||||
|
||||
@overrides(BasePreferencePane)
|
||||
def create_pane(self, core_conf, console_config):
|
||||
self.add_header(_("New Torrents"))
|
||||
self.add_checked_input("queue_new_to_top", _("Queue to top"), core_conf["queue_new_to_top"])
|
||||
self.add_header(_("Active Torrents"), True)
|
||||
self.add_int_spin_input("max_active_limit", "%s:" % _("Total"), core_conf["max_active_limit"],
|
||||
min_val=-1, max_val=9999)
|
||||
self.add_int_spin_input("max_active_downloading", "%s:" % _("Downloading"),
|
||||
core_conf["max_active_downloading"], min_val=-1, max_val=9999)
|
||||
self.add_int_spin_input("max_active_seeding", "%s:" % _("Seeding"),
|
||||
core_conf["max_active_seeding"], min_val=-1, max_val=9999)
|
||||
self.add_checked_input("dont_count_slow_torrents", "Ignore slow torrents",
|
||||
core_conf["dont_count_slow_torrents"])
|
||||
self.add_checked_input("auto_manage_prefer_seeds", "Prefer seeding torrents",
|
||||
core_conf["auto_manage_prefer_seeds"])
|
||||
self.add_header(_("Seeding Rotation"), space_above=True)
|
||||
self.add_float_spin_input("share_ratio_limit", "%s:" % _("Share Ratio"),
|
||||
core_conf["share_ratio_limit"], precision=2, min_val=-1.0, max_val=100.0)
|
||||
self.add_float_spin_input("seed_time_ratio_limit", "%s:" % _("Time Ratio"),
|
||||
core_conf["seed_time_ratio_limit"], precision=2, min_val=-1.0, max_val=100.0)
|
||||
self.add_int_spin_input("seed_time_limit", "%s:" % _("Time (m)"), core_conf["seed_time_limit"],
|
||||
min_val=1, max_val=10000)
|
||||
seedratio = FloatSpinInput(self.mode, "stop_seed_ratio", "", self.move, core_conf["stop_seed_ratio"],
|
||||
precision=2, inc_amt=0.1, min_val=0.5, max_val=100.0)
|
||||
self.add_checkedplus_input("stop_seed_at_ratio", "%s:" % _("Share Ratio Reached"), seedratio,
|
||||
core_conf["stop_seed_at_ratio"])
|
||||
self.add_checked_input("remove_seed_at_ratio", _("Remove torrent (Unchecked pauses torrent)"),
|
||||
core_conf["remove_seed_at_ratio"])
|
||||
|
||||
|
||||
class ProxyPane(BasePreferencePane):
|
||||
|
||||
def __init__(self, preferences):
|
||||
BasePreferencePane.__init__(self, " %s " % _("Proxy"), preferences)
|
||||
|
||||
@overrides(BasePreferencePane)
|
||||
def create_pane(self, core_conf, console_config):
|
||||
self.add_header(_("Proxy Settings"))
|
||||
self.add_header(_("Proxy"), space_above=True)
|
||||
proxy = core_conf["proxy"]
|
||||
self.add_int_spin_input("proxy_type", "%s:" % _("Type"), proxy["type"], min_val=0, max_val=5)
|
||||
self.add_info_field("proxy_info_1", " 0: None 1: Socks4 2: Socks5", "")
|
||||
self.add_info_field("proxy_info_2", " 3: Socks5 Auth 4: HTTP 5: HTTP Auth", "")
|
||||
self.add_text_input("proxy_username", "%s:" % _("Username"), proxy["username"])
|
||||
self.add_text_input("proxy_password", "%s:" % _("Password"), proxy["password"])
|
||||
self.add_text_input("proxy_hostname", "%s:" % _("Hostname"), proxy["hostname"])
|
||||
self.add_int_spin_input("proxy_port", "%s:" % _("Port"), proxy["port"], min_val=0, max_val=65535)
|
||||
self.add_checked_input("proxy_hostnames", _("Proxy hostnames"), proxy["proxy_hostnames"])
|
||||
self.add_checked_input("proxy_peer_connections", _("Proxy peer connections"), proxy["proxy_peer_connections"])
|
||||
|
||||
self.add_header(_("I2P Proxy"), space_above=True)
|
||||
self.add_text_input("i2p_hostname", "%s:" % _("Hostname"),
|
||||
core_conf["i2p_proxy"]["hostname"])
|
||||
self.add_int_spin_input("i2p_port",
|
||||
"%s:" % _("Port"), core_conf["i2p_proxy"]["port"], min_val=0, max_val=65535)
|
||||
self.add_checked_input("anonymous_mode", _("Anonymous Mode"), core_conf["anonymous_mode"])
|
||||
|
||||
|
||||
class CachePane(BasePreferencePane):
|
||||
|
||||
def __init__(self, preferences):
|
||||
BasePreferencePane.__init__(self, " %s " % _("Cache"), preferences)
|
||||
self.created = False
|
||||
|
||||
@overrides(BasePreferencePane)
|
||||
def create_pane(self, core_conf, console_config):
|
||||
self.core_conf = core_conf
|
||||
|
||||
def build_pane(self, core_conf, status):
|
||||
self.created = True
|
||||
self.add_header(_("Settings"), space_below=True)
|
||||
self.add_int_spin_input("cache_size",
|
||||
"%s:" % _("Cache Size (16 KiB blocks)"), core_conf["cache_size"],
|
||||
min_val=0, max_val=99999)
|
||||
self.add_int_spin_input("cache_expiry",
|
||||
"%s:" % _("Cache Expiry (seconds)"), core_conf["cache_expiry"],
|
||||
min_val=1, max_val=32000)
|
||||
self.add_header(" %s" % _("Write"), space_above=True)
|
||||
self.add_info_field("blocks_written", " %s:" % _("Blocks Written"), status["blocks_written"])
|
||||
self.add_info_field("writes", " %s:" % _("Writes"), status["writes"])
|
||||
self.add_info_field("write_hit_ratio",
|
||||
" %s:" % _("Write Cache Hit Ratio"), "%.2f" % status["write_hit_ratio"])
|
||||
self.add_header(" %s" % _("Read"))
|
||||
self.add_info_field("blocks_read",
|
||||
" %s:" % _("Blocks Read"), status["blocks_read"])
|
||||
self.add_info_field("blocks_read_hit",
|
||||
" %s:" % _("Blocks Read hit"), status["blocks_read_hit"])
|
||||
self.add_info_field("reads",
|
||||
" %s:" % _("Reads"), status["reads"])
|
||||
self.add_info_field("read_hit_ratio",
|
||||
" %s:" % _("Read Cache Hit Ratio"), "%.2f" % status["read_hit_ratio"])
|
||||
self.add_header(" %s" % _("Size"))
|
||||
self.add_info_field("cache_size_info",
|
||||
" %s:" % _("Cache Size"), status["cache_size"])
|
||||
self.add_info_field("read_cache_size",
|
||||
" %s:" % _("Read Cache Size"), status["read_cache_size"])
|
||||
|
||||
@overrides(BasePreferencePane)
|
||||
def update(self, active):
|
||||
if active:
|
||||
client.core.get_cache_status().addCallback(self.update_cache_status_fields)
|
||||
|
||||
def update_cache_status_fields(self, status):
|
||||
if not self.created:
|
||||
self.build_pane(self.core_conf, status)
|
||||
else:
|
||||
for ipt in self.inputs:
|
||||
if not ipt.has_input() and ipt.name in status:
|
||||
ipt.set_value(status[ipt.name])
|
||||
self.preferences.refresh()
|
333
deluge/ui/console/modes/preferences/preferences.py
Normal file
333
deluge/ui/console/modes/preferences/preferences.py
Normal file
@ -0,0 +1,333 @@
|
||||
# -*- 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.
|
||||
#
|
||||
|
||||
import logging
|
||||
from collections import deque
|
||||
|
||||
import deluge.component as component
|
||||
from deluge.decorators import overrides
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console.modes.basemode import BaseMode
|
||||
from deluge.ui.console.modes.preferences.preference_panes import (BandwidthPane, CachePane, DaemonPane, DownloadsPane,
|
||||
InterfacePane, NetworkPane, OtherPane, ProxyPane,
|
||||
QueuePane)
|
||||
from deluge.ui.console.utils import curses_util as util
|
||||
from deluge.ui.console.widgets.fields import SelectInput
|
||||
from deluge.ui.console.widgets.popup import MessagePopup, PopupsHandler
|
||||
from deluge.ui.console.widgets.sidebar import Sidebar
|
||||
|
||||
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 lets you view and configure various options in deluge.
|
||||
|
||||
There are three main sections to this screen. Only one section is active at a time. \
|
||||
You can switch the active section by hitting TAB (or Shift-TAB to go back one)
|
||||
|
||||
The section on the left displays the various categories that the settings fall in. \
|
||||
You can navigate the list using the up/down arrows
|
||||
|
||||
The section on the right shows the settings for the selected category. When this \
|
||||
section is active you can navigate the various settings with the up/down arrows. \
|
||||
Special keys for each input type are described below.
|
||||
|
||||
The final section is at the bottom right, the: [Cancel] [Apply] [OK] buttons.
|
||||
When this section is active, simply select the option you want using the arrow
|
||||
keys and press Enter to confim.
|
||||
|
||||
|
||||
Special keys for various input types are as follows:
|
||||
- For text inputs you can simply type in the value.
|
||||
|
||||
{|indent: |}- For numeric inputs (indicated by the value being in []s), you can type a value, \
|
||||
or use PageUp and PageDown to increment/decrement the value.
|
||||
|
||||
- For checkbox inputs use the spacebar to toggle
|
||||
|
||||
{|indent: |}- For checkbox plus something else inputs (the something else being only visible \
|
||||
when you check the box) you can toggle the check with space, use the right \
|
||||
arrow to edit the other value, and escape to get back to the check box.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class ZONE(object):
|
||||
length = 3
|
||||
CATEGORIES, PREFRENCES, ACTIONS = range(length)
|
||||
|
||||
|
||||
class PreferenceSidebar(Sidebar):
|
||||
|
||||
def __init__(self, torrentview, width):
|
||||
height = curses.LINES - 2
|
||||
Sidebar.__init__(self, torrentview, width, height, title=None, border_off_north=1)
|
||||
self.categories = [_("Interface"), _("Downloads"), _("Network"), _("Bandwidth"),
|
||||
_("Other"), _("Daemon"), _("Queue"), _("Proxy"), _("Cache")]
|
||||
for name in self.categories:
|
||||
self.add_text_field(name, name, selectable=True, font_unfocused_active="bold",
|
||||
color_unfocused_active="white,black")
|
||||
|
||||
def on_resize(self):
|
||||
self.resize_window(curses.LINES - 2, self.width)
|
||||
|
||||
|
||||
class Preferences(BaseMode, PopupsHandler):
|
||||
|
||||
def __init__(self, parent_mode, stdscr, console_config, encoding=None):
|
||||
BaseMode.__init__(self, stdscr, encoding=encoding, do_refresh=False)
|
||||
PopupsHandler.__init__(self)
|
||||
self.parent_mode = parent_mode
|
||||
self.cur_cat = 0
|
||||
self.messages = deque()
|
||||
self.action_input = None
|
||||
self.config_loaded = False
|
||||
self.console_config = console_config
|
||||
self.active_port = -1
|
||||
self.active_zone = ZONE.CATEGORIES
|
||||
self.sidebar_width = 15 # Width of the categories pane
|
||||
|
||||
self.sidebar = PreferenceSidebar(parent_mode, self.sidebar_width)
|
||||
self.sidebar.set_focused(True)
|
||||
self.sidebar.active_input = 0
|
||||
|
||||
self._calc_sizes(resize=False)
|
||||
|
||||
self.panes = [
|
||||
InterfacePane(self),
|
||||
DownloadsPane(self),
|
||||
NetworkPane(self),
|
||||
BandwidthPane(self),
|
||||
OtherPane(self),
|
||||
DaemonPane(self),
|
||||
QueuePane(self),
|
||||
ProxyPane(self),
|
||||
CachePane(self)
|
||||
]
|
||||
|
||||
self.action_input = SelectInput(self, None, None, [_("Cancel"), _("Apply"), _("OK")], [0, 1, 2], 0)
|
||||
|
||||
def load_config(self):
|
||||
if self.config_loaded:
|
||||
return
|
||||
|
||||
def on_get_config(core_config):
|
||||
self.core_config = core_config
|
||||
self.config_loaded = True
|
||||
for p in self.panes:
|
||||
p.create_pane(core_config, self.console_config)
|
||||
self.refresh()
|
||||
client.core.get_config().addCallback(on_get_config)
|
||||
|
||||
def on_get_listen_port(port):
|
||||
self.active_port = port
|
||||
client.core.get_listen_port().addCallback(on_get_listen_port)
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
# top/bottom bars: 2, Action buttons (Cancel/Apply/OK): 1
|
||||
return self.rows - 3
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self.prefs_width
|
||||
|
||||
def _calc_sizes(self, resize=True):
|
||||
self.prefs_width = self.cols - self.sidebar_width
|
||||
|
||||
if not resize:
|
||||
return
|
||||
|
||||
for p in self.panes:
|
||||
p.resize_window(self.height, p.pane_width)
|
||||
|
||||
def _draw_preferences(self):
|
||||
self.cur_cat = self.sidebar.active_input
|
||||
self.panes[self.cur_cat].render(self, self.stdscr, self.prefs_width, self.active_zone == ZONE.PREFRENCES)
|
||||
self.panes[self.cur_cat].refresh()
|
||||
|
||||
def _draw_actions(self):
|
||||
selected = self.active_zone == ZONE.ACTIONS
|
||||
self.stdscr.hline(self.rows - 3, self.sidebar_width, "_", self.cols)
|
||||
self.action_input.render(self.stdscr, self.rows - 2, width=self.cols,
|
||||
active=selected, focus=True, col=self.cols - 22)
|
||||
|
||||
@overrides(BaseMode)
|
||||
def on_resize(self, rows, cols):
|
||||
BaseMode.on_resize(self, rows, cols)
|
||||
self._calc_sizes()
|
||||
|
||||
if self.popup:
|
||||
self.popup.handle_resize()
|
||||
|
||||
self.sidebar.on_resize()
|
||||
self.refresh()
|
||||
|
||||
@overrides(component.Component)
|
||||
def update(self):
|
||||
for i, p in enumerate(self.panes):
|
||||
self.panes[i].update(i == self.cur_cat)
|
||||
|
||||
@overrides(BaseMode)
|
||||
def resume(self):
|
||||
BaseMode.resume(self)
|
||||
self.sidebar.show()
|
||||
|
||||
@overrides(BaseMode)
|
||||
def refresh(self):
|
||||
if not component.get("ConsoleUI").is_active_mode(self) or not self.config_loaded:
|
||||
return
|
||||
|
||||
if self.popup is None and self.messages:
|
||||
title, msg = self.messages.popleft()
|
||||
self.push_popup(MessagePopup(self, title, msg))
|
||||
|
||||
self.stdscr.erase()
|
||||
self.draw_statusbars()
|
||||
self._draw_actions()
|
||||
# Necessary to force updating the stdscr
|
||||
self.stdscr.noutrefresh()
|
||||
|
||||
self.sidebar.refresh()
|
||||
|
||||
# do this last since it moves the cursor
|
||||
self._draw_preferences()
|
||||
|
||||
if self.popup:
|
||||
self.popup.refresh()
|
||||
|
||||
curses.doupdate()
|
||||
|
||||
def _apply_prefs(self):
|
||||
if self.core_config is None:
|
||||
return
|
||||
|
||||
def update_conf_value(key, source_dict, dest_dict, updated):
|
||||
if dest_dict[key] != source_dict[key]:
|
||||
dest_dict[key] = source_dict[key]
|
||||
updated = True
|
||||
return updated
|
||||
|
||||
new_core_config = {}
|
||||
for pane in self.panes:
|
||||
if not isinstance(pane, InterfacePane):
|
||||
pane.add_config_values(new_core_config)
|
||||
# Apply Core Prefs
|
||||
if client.connected():
|
||||
# Only do this if we're connected to a daemon
|
||||
config_to_set = {}
|
||||
for key in new_core_config:
|
||||
# The values do not match so this needs to be updated
|
||||
if self.core_config[key] != new_core_config[key]:
|
||||
config_to_set[key] = new_core_config[key]
|
||||
|
||||
if config_to_set:
|
||||
# Set each changed config value in the core
|
||||
client.core.set_config(config_to_set)
|
||||
client.force_call(True)
|
||||
# Update the configuration
|
||||
self.core_config.update(config_to_set)
|
||||
|
||||
# Update Interface Prefs
|
||||
new_console_config = {}
|
||||
didupdate = False
|
||||
for pane in self.panes:
|
||||
# could just access panes by index, but that would break if panes
|
||||
# are ever reordered, so do it the slightly slower but safer way
|
||||
if isinstance(pane, InterfacePane):
|
||||
pane.add_config_values(new_console_config)
|
||||
for k in ["ring_bell", "language"]:
|
||||
didupdate = update_conf_value(k, new_console_config, self.console_config, didupdate)
|
||||
for k in ["separate_complete", "move_selection"]:
|
||||
didupdate = update_conf_value(k, new_console_config, self.console_config["torrentview"], didupdate)
|
||||
for k in ["ignore_duplicate_lines", "save_command_history",
|
||||
"third_tab_lists_all", "torrents_per_tab_press"]:
|
||||
didupdate = update_conf_value(k, new_console_config, self.console_config["cmdline"], didupdate)
|
||||
|
||||
if didupdate:
|
||||
self.parent_mode.on_config_changed()
|
||||
|
||||
def _update_preferences(self, core_config):
|
||||
self.core_config = core_config
|
||||
for pane in self.panes:
|
||||
pane.update_values(core_config)
|
||||
|
||||
def _actions_read(self, c):
|
||||
self.action_input.handle_read(c)
|
||||
if c in [curses.KEY_ENTER, util.KEY_ENTER2]:
|
||||
# take action
|
||||
if self.action_input.selected_index == 0: # Cancel
|
||||
self.back_to_parent()
|
||||
elif self.action_input.selected_index == 1: # Apply
|
||||
self._apply_prefs()
|
||||
client.core.get_config().addCallback(self._update_preferences)
|
||||
elif self.action_input.selected_index == 2: # OK
|
||||
self._apply_prefs()
|
||||
self.back_to_parent()
|
||||
|
||||
def back_to_parent(self):
|
||||
component.get("ConsoleUI").set_mode(self.parent_mode.mode_name)
|
||||
|
||||
@overrides(BaseMode)
|
||||
def read_input(self):
|
||||
c = self.stdscr.getch()
|
||||
|
||||
if self.popup:
|
||||
if self.popup.handle_read(c):
|
||||
self.pop_popup()
|
||||
self.refresh()
|
||||
return
|
||||
|
||||
if util.is_printable_char(c):
|
||||
if chr(c) == "Q":
|
||||
from twisted.internet import reactor
|
||||
if client.connected():
|
||||
def on_disconnect(result):
|
||||
reactor.stop()
|
||||
client.disconnect().addCallback(on_disconnect)
|
||||
else:
|
||||
reactor.stop()
|
||||
return
|
||||
elif chr(c) == "h":
|
||||
self.push_popup(MessagePopup(self, "Preferences Help", HELP_STR))
|
||||
|
||||
if self.sidebar.has_focus() and c == util.KEY_ESC:
|
||||
self.back_to_parent()
|
||||
return
|
||||
|
||||
def update_active_zone(val):
|
||||
self.active_zone += val
|
||||
if self.active_zone == -1:
|
||||
self.active_zone = ZONE.length - 1
|
||||
else:
|
||||
self.active_zone %= ZONE.length
|
||||
self.sidebar.set_focused(self.active_zone == ZONE.CATEGORIES)
|
||||
|
||||
if c == util.KEY_TAB:
|
||||
update_active_zone(1)
|
||||
elif c == curses.KEY_BTAB:
|
||||
update_active_zone(-1)
|
||||
else:
|
||||
if self.active_zone == ZONE.CATEGORIES:
|
||||
self.sidebar.handle_read(c)
|
||||
elif self.active_zone == ZONE.PREFRENCES:
|
||||
self.panes[self.cur_cat].handle_read(c)
|
||||
elif self.active_zone == ZONE.ACTIONS:
|
||||
self._actions_read(c)
|
||||
|
||||
self.refresh()
|
||||
|
||||
def is_active_pane(self, pane):
|
||||
return pane == self.panes[self.cur_cat]
|
@ -1,328 +0,0 @@
|
||||
# -*- 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.
|
||||
#
|
||||
|
||||
import logging
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
import deluge.component as component
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console import colors
|
||||
from deluge.ui.console.modes.input_popup import InputPopup
|
||||
from deluge.ui.console.modes.popup import Popup, SelectablePopup
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
torrent_options = [
|
||||
("max_download_speed", float),
|
||||
("max_upload_speed", float),
|
||||
("max_connections", int),
|
||||
("max_upload_slots", int),
|
||||
("prioritize_first_last", bool),
|
||||
("sequential_download", bool),
|
||||
("is_auto_managed", bool),
|
||||
("stop_at_ratio", bool),
|
||||
("stop_ratio", float),
|
||||
("remove_at_ratio", bool),
|
||||
("move_on_completed", bool),
|
||||
("move_on_completed_path", str)
|
||||
]
|
||||
|
||||
torrent_options_to_names = {
|
||||
"max_download_speed": "Max DL speed",
|
||||
"max_upload_speed": "Max UL speed",
|
||||
"max_connections": "Max connections",
|
||||
"max_upload_slots": "Max upload slots",
|
||||
"prioritize_first_last": "Prioritize first/last pieces",
|
||||
"sequential_download": "Sequential download",
|
||||
"is_auto_managed": "Is auto managed?",
|
||||
"stop_at_ratio": "Stop at ratio",
|
||||
"stop_ratio": "Seeding ratio limit",
|
||||
"remove_at_ratio": "Remove after reaching ratio",
|
||||
"move_on_completed": "Move torrent after completion",
|
||||
"move_on_completed_path": "Folder to move the torrent to"
|
||||
}
|
||||
|
||||
|
||||
class ACTION(object):
|
||||
PAUSE = 0
|
||||
RESUME = 1
|
||||
REANNOUNCE = 2
|
||||
EDIT_TRACKERS = 3
|
||||
RECHECK = 4
|
||||
REMOVE = 5
|
||||
REMOVE_DATA = 6
|
||||
REMOVE_NODATA = 7
|
||||
DETAILS = 8
|
||||
MOVE_STORAGE = 9
|
||||
QUEUE = 10
|
||||
QUEUE_TOP = 11
|
||||
QUEUE_UP = 12
|
||||
QUEUE_DOWN = 13
|
||||
QUEUE_BOTTOM = 14
|
||||
TORRENT_OPTIONS = 15
|
||||
|
||||
|
||||
def action_error(error, mode):
|
||||
rerr = error.value
|
||||
mode.report_message("An Error Occurred", "%s got error %s: %s" % (
|
||||
rerr.method, rerr.exception_type, rerr.exception_msg))
|
||||
mode.refresh()
|
||||
|
||||
|
||||
def torrent_action(idx, data, mode, ids):
|
||||
if ids:
|
||||
if data == ACTION.PAUSE:
|
||||
log.debug("Pausing torrents: %s", ids)
|
||||
client.core.pause_torrent(ids).addErrback(action_error, mode)
|
||||
elif data == ACTION.RESUME:
|
||||
log.debug("Resuming torrents: %s", ids)
|
||||
client.core.resume_torrent(ids).addErrback(action_error, mode)
|
||||
elif data == ACTION.QUEUE:
|
||||
def do_queue(idx, qact, mode, ids):
|
||||
def move_selection(r):
|
||||
if mode.config["move_selection"]:
|
||||
queue_length = 0
|
||||
selected_num = 0
|
||||
for tid in mode.curstate:
|
||||
tq = mode.curstate.get(tid)["queue"]
|
||||
if tq != -1:
|
||||
queue_length += 1
|
||||
if tq in mode.marked:
|
||||
selected_num += 1
|
||||
if qact == ACTION.QUEUE_TOP:
|
||||
if mode.marked:
|
||||
mode.cursel = 1 + sorted(mode.marked).index(mode.cursel)
|
||||
else:
|
||||
mode.cursel = 1
|
||||
mode.marked = range(1, selected_num + 1)
|
||||
elif qact == ACTION.QUEUE_UP:
|
||||
mode.cursel = max(1, mode.cursel - 1)
|
||||
mode.marked = [marked - 1 for marked in mode.marked]
|
||||
mode.marked = [marked for marked in mode.marked if marked > 0]
|
||||
elif qact == ACTION.QUEUE_DOWN:
|
||||
mode.cursel = min(queue_length, mode.cursel + 1)
|
||||
mode.marked = [marked + 1 for marked in mode.marked]
|
||||
mode.marked = [marked for marked in mode.marked if marked <= queue_length]
|
||||
elif qact == ACTION.QUEUE_BOTTOM:
|
||||
if mode.marked:
|
||||
mode.cursel = queue_length - selected_num + 1 + sorted(mode.marked).index(mode.cursel)
|
||||
else:
|
||||
mode.cursel = queue_length
|
||||
mode.marked = range(queue_length - selected_num + 1, queue_length + 1)
|
||||
|
||||
if qact == ACTION.QUEUE_TOP:
|
||||
log.debug("Queuing torrents top")
|
||||
client.core.queue_top(ids).addCallback(move_selection)
|
||||
elif qact == ACTION.QUEUE_UP:
|
||||
log.debug("Queuing torrents up")
|
||||
client.core.queue_up(ids).addCallback(move_selection)
|
||||
elif qact == ACTION.QUEUE_DOWN:
|
||||
log.debug("Queuing torrents down")
|
||||
client.core.queue_down(ids).addCallback(move_selection)
|
||||
elif qact == ACTION.QUEUE_BOTTOM:
|
||||
log.debug("Queuing torrents bottom")
|
||||
client.core.queue_bottom(ids).addCallback(move_selection)
|
||||
|
||||
if len(ids) == 1:
|
||||
mode.clear_marks()
|
||||
return True
|
||||
popup = SelectablePopup(mode, "Queue Action", do_queue, (mode, ids))
|
||||
popup.add_line("_Top", data=ACTION.QUEUE_TOP)
|
||||
popup.add_line("_Up", data=ACTION.QUEUE_UP)
|
||||
popup.add_line("_Down", data=ACTION.QUEUE_DOWN)
|
||||
popup.add_line("_Bottom", data=ACTION.QUEUE_BOTTOM)
|
||||
mode.set_popup(popup)
|
||||
return False
|
||||
elif data == ACTION.REMOVE:
|
||||
def do_remove(data):
|
||||
if not data:
|
||||
return
|
||||
mode.clear_marks()
|
||||
|
||||
remove_data = data["remove_files"]
|
||||
|
||||
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(ids, remove_data)
|
||||
d.addCallback(on_removed_finished)
|
||||
|
||||
def got_status(status):
|
||||
return (status["name"], status["state"])
|
||||
|
||||
callbacks = []
|
||||
for tid in ids:
|
||||
d = client.core.get_torrent_status(tid, ["name", "state"])
|
||||
callbacks.append(d.addCallback(got_status))
|
||||
|
||||
def finish_up(status):
|
||||
status = [t_status[1] for t_status in status]
|
||||
|
||||
if len(ids) == 1:
|
||||
rem_msg = "{!info!}Removing the following torrent:{!input!}"
|
||||
else:
|
||||
rem_msg = "{!info!}Removing the following torrents:{!input!}"
|
||||
|
||||
for i, (name, state) in enumerate(status):
|
||||
color = colors.state_color[state]
|
||||
rem_msg += "\n %s* {!input!}%s" % (color, name)
|
||||
if i == 5:
|
||||
if i < len(status):
|
||||
rem_msg += "\n {!red!}And %i more" % (len(status) - 5)
|
||||
break
|
||||
|
||||
popup = InputPopup(mode, "(Esc to cancel, Enter to remove)", close_cb=do_remove)
|
||||
popup.add_text(rem_msg)
|
||||
popup.add_spaces(1)
|
||||
popup.add_select_input("{!info!}Torrent files:", "remove_files",
|
||||
["Keep", "Remove"], [False, True], False)
|
||||
mode.set_popup(popup)
|
||||
defer.DeferredList(callbacks).addCallback(finish_up)
|
||||
return False
|
||||
elif data == ACTION.MOVE_STORAGE:
|
||||
def do_move(res):
|
||||
import os.path
|
||||
if os.path.exists(res["path"]) and not os.path.isdir(res["path"]):
|
||||
mode.report_message("Cannot Move Download Folder",
|
||||
"{!error!}%s exists and is not a directory" % res["path"])
|
||||
else:
|
||||
log.debug("Moving %s to: %s", ids, res["path"])
|
||||
client.core.move_storage(ids, res["path"]).addErrback(action_error, mode)
|
||||
if len(ids) == 1:
|
||||
mode.clear_marks()
|
||||
return True
|
||||
popup = InputPopup(mode, "Move Download Folder (Esc to cancel)", close_cb=do_move)
|
||||
popup.add_text_input("Enter path to move to:", "path")
|
||||
mode.set_popup(popup)
|
||||
return False
|
||||
elif data == ACTION.RECHECK:
|
||||
log.debug("Rechecking torrents: %s", ids)
|
||||
client.core.force_recheck(ids).addErrback(action_error, mode)
|
||||
elif data == ACTION.REANNOUNCE:
|
||||
log.debug("Reannouncing torrents: %s", ids)
|
||||
client.core.force_reannounce(ids).addErrback(action_error, mode)
|
||||
elif data == ACTION.DETAILS:
|
||||
log.debug("Torrent details")
|
||||
tid = mode.current_torrent_id()
|
||||
if tid:
|
||||
mode.show_torrent_details(tid)
|
||||
else:
|
||||
log.error("No current torrent in _torrent_action, this is a bug")
|
||||
elif data == ACTION.TORRENT_OPTIONS:
|
||||
mode.popup = Popup(mode, "Torrent options")
|
||||
mode.popup.add_line("Querying core, please wait...")
|
||||
|
||||
torrents = ids
|
||||
|
||||
options = {}
|
||||
|
||||
def _do_set_torrent_options(ids, result):
|
||||
options = {}
|
||||
for opt in result:
|
||||
if result[opt] not in ["multiple", None]:
|
||||
options[opt] = result[opt]
|
||||
client.core.set_torrent_options(ids, options)
|
||||
for tid in ids:
|
||||
if "move_on_completed_path" in options:
|
||||
client.core.set_torrent_move_completed_path(tid, options["move_on_completed_path"])
|
||||
if "move_on_completed" in options:
|
||||
client.core.set_torrent_move_completed(tid, options["move_on_completed"])
|
||||
if "is_auto_managed" in options:
|
||||
client.core.set_torrent_auto_managed(tid, options["is_auto_managed"])
|
||||
if "remove_at_ratio" in options:
|
||||
client.core.set_torrent_remove_at_ratio(tid, options["remove_at_ratio"])
|
||||
if "prioritize_first_last" in options:
|
||||
client.core.set_torrent_prioritize_first_last(tid, options["prioritize_first_last"])
|
||||
|
||||
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):
|
||||
def cb(result, ids=ids):
|
||||
return _do_set_torrent_options(ids, result)
|
||||
|
||||
option_popup = InputPopup(mode, "Set torrent options (Esc to cancel)", close_cb=cb, height_req=22)
|
||||
|
||||
for (field, field_type) in torrent_options:
|
||||
caption = "{!info!}" + torrent_options_to_names[field]
|
||||
value = options[field]
|
||||
if field_type == str:
|
||||
if not isinstance(value, basestring):
|
||||
value = str(value)
|
||||
option_popup.add_text_input(caption, field, value)
|
||||
elif field_type == bool:
|
||||
if options[field] == "multiple":
|
||||
choices = (
|
||||
["Yes", "No", "Mixed"],
|
||||
[True, False, None],
|
||||
2
|
||||
)
|
||||
else:
|
||||
choices = (
|
||||
["Yes", "No"],
|
||||
[True, False],
|
||||
[True, False].index(options[field])
|
||||
)
|
||||
option_popup.add_select_input(caption, field, choices[0], choices[1], choices[2])
|
||||
elif field_type == float:
|
||||
option_popup.add_float_spin_input(caption, field, value, min_val=-1)
|
||||
elif field_type == int:
|
||||
option_popup.add_int_spin_input(caption, field, value, min_val=-1)
|
||||
|
||||
mode.set_popup(option_popup)
|
||||
mode.refresh()
|
||||
|
||||
callbacks = []
|
||||
|
||||
field_list = [torrent_option[0] for torrent_option in torrent_options]
|
||||
|
||||
for tid in torrents:
|
||||
deferred = component.get("SessionProxy").get_torrent_status(tid, field_list)
|
||||
callbacks.append(deferred.addCallback(on_torrent_status))
|
||||
|
||||
callbacks = defer.DeferredList(callbacks)
|
||||
callbacks.addCallback(create_popup)
|
||||
|
||||
if len(ids) == 1:
|
||||
mode.clear_marks()
|
||||
return True
|
||||
|
||||
|
||||
# Creates the popup. mode is the calling mode, tids is a list of torrents to take action upon
|
||||
def torrent_actions_popup(mode, tids, details=False, action=None):
|
||||
if action is not None:
|
||||
torrent_action(-1, action, mode, tids)
|
||||
return
|
||||
popup = SelectablePopup(mode, "Torrent Actions", torrent_action, (mode, tids))
|
||||
popup.add_line("_Pause", data=ACTION.PAUSE)
|
||||
popup.add_line("_Resume", data=ACTION.RESUME)
|
||||
if details:
|
||||
popup.add_divider()
|
||||
popup.add_line("Queue", data=ACTION.QUEUE)
|
||||
popup.add_divider()
|
||||
popup.add_line("_Update Tracker", data=ACTION.REANNOUNCE)
|
||||
popup.add_divider()
|
||||
popup.add_line("Remo_ve Torrent", data=ACTION.REMOVE)
|
||||
popup.add_line("_Force Recheck", data=ACTION.RECHECK)
|
||||
popup.add_line("_Move Download Folder", data=ACTION.MOVE_STORAGE)
|
||||
popup.add_divider()
|
||||
if details:
|
||||
popup.add_line("Torrent _Details", data=ACTION.DETAILS)
|
||||
popup.add_line("Torrent _Options", data=ACTION.TORRENT_OPTIONS)
|
||||
mode.set_popup(popup)
|
@ -10,17 +10,18 @@
|
||||
from __future__ import division
|
||||
|
||||
import logging
|
||||
from collections import deque
|
||||
|
||||
import deluge.component as component
|
||||
from deluge.common import FILE_PRIORITY, fdate, fsize, ftime
|
||||
from deluge.common import FILE_PRIORITY, fsize
|
||||
from deluge.decorators import overrides
|
||||
from deluge.ui.client import client
|
||||
from deluge.ui.console import colors
|
||||
from deluge.ui.console.modes import format_utils
|
||||
from deluge.ui.console.modes.basemode import BaseMode
|
||||
from deluge.ui.console.modes.input_popup import InputPopup
|
||||
from deluge.ui.console.modes.popup import MessagePopup, SelectablePopup
|
||||
from deluge.ui.console.modes.torrent_actions import ACTION, torrent_actions_popup
|
||||
from deluge.ui.console.modes.torrentlist.torrentactions import ACTION, torrent_actions_popup
|
||||
from deluge.ui.console.utils import curses_util as util
|
||||
from deluge.ui.console.utils import colors
|
||||
from deluge.ui.console.utils.column import get_column_value, torrent_data_fields
|
||||
from deluge.ui.console.utils.format_utils import format_priority, format_progress, format_row
|
||||
from deluge.ui.console.widgets.popup import InputPopup, MessagePopup, PopupsHandler, SelectablePopup
|
||||
|
||||
try:
|
||||
import curses
|
||||
@ -63,42 +64,35 @@ download priority of selected files and folders.
|
||||
"""
|
||||
|
||||
|
||||
class TorrentDetail(BaseMode, component.Component):
|
||||
def __init__(self, alltorrentmode, torrentid, stdscr, console_config, encoding=None):
|
||||
class TorrentDetail(BaseMode, PopupsHandler):
|
||||
|
||||
def __init__(self, parent_mode, stdscr, console_config, encoding=None):
|
||||
PopupsHandler.__init__(self)
|
||||
self.console_config = console_config
|
||||
self.alltorrentmode = alltorrentmode
|
||||
self.torrentid = torrentid
|
||||
self.parent_mode = parent_mode
|
||||
self.torrentid = None
|
||||
self.torrent_state = None
|
||||
self.popup = None
|
||||
self.messages = deque()
|
||||
self._status_keys = ["files", "name", "state", "download_payload_rate", "upload_payload_rate",
|
||||
"progress", "eta", "all_time_download", "total_uploaded", "ratio",
|
||||
"num_seeds", "total_seeds", "num_peers", "total_peers", "active_time",
|
||||
"seeding_time", "time_added", "distributed_copies", "num_pieces",
|
||||
"piece_length", "download_location", "file_progress", "file_priorities", "message",
|
||||
"total_wanted", "tracker_host", "owner"]
|
||||
|
||||
"total_wanted", "tracker_host", "owner", "seed_rank", "last_seen_complete",
|
||||
"completed_time"]
|
||||
self.file_list = None
|
||||
self.current_file = None
|
||||
self.current_file_idx = 0
|
||||
self.file_off = 0
|
||||
self.more_to_draw = False
|
||||
self.full_names = None
|
||||
|
||||
self.column_string = ""
|
||||
self.files_sep = None
|
||||
|
||||
self.marked = {}
|
||||
|
||||
BaseMode.__init__(self, stdscr, encoding)
|
||||
component.Component.__init__(self, "TorrentDetail", 1, depend=["SessionProxy"])
|
||||
|
||||
self.column_names = ["Filename", "Size", "Progress", "Priority"]
|
||||
self.__update_columns()
|
||||
|
||||
component.start(["TorrentDetail"])
|
||||
|
||||
self._listing_start = self.rows // 2
|
||||
self._listing_space = self._listing_start - self._listing_start
|
||||
|
||||
@ -106,18 +100,44 @@ class TorrentDetail(BaseMode, component.Component):
|
||||
client.register_event_handler("TorrentFolderRenamedEvent", self._on_torrentfolderrenamed_event)
|
||||
client.register_event_handler("TorrentRemovedEvent", self._on_torrentremoved_event)
|
||||
|
||||
curses.curs_set(0)
|
||||
util.safe_curs_set(util.Curser.INVISIBLE)
|
||||
self.stdscr.notimeout(0)
|
||||
|
||||
# component start/update
|
||||
def start(self):
|
||||
component.get("SessionProxy").get_torrent_status(self.torrentid, self._status_keys).addCallback(self.set_state)
|
||||
def set_torrent_id(self, torrentid):
|
||||
self.torrentid = torrentid
|
||||
self.file_list = None
|
||||
|
||||
def update(self):
|
||||
component.get("SessionProxy").get_torrent_status(self.torrentid, self._status_keys).addCallback(self.set_state)
|
||||
def back_to_overview(self):
|
||||
component.get("ConsoleUI").set_mode(self.parent_mode.mode_name)
|
||||
|
||||
@overrides(component.Component)
|
||||
def start(self):
|
||||
self.update()
|
||||
|
||||
@overrides(component.Component)
|
||||
def update(self, torrentid=None):
|
||||
if torrentid:
|
||||
self.set_torrent_id(torrentid)
|
||||
|
||||
if self.torrentid:
|
||||
component.get("SessionProxy").get_torrent_status(self.torrentid,
|
||||
self._status_keys).addCallback(self.set_state)
|
||||
|
||||
@overrides(BaseMode)
|
||||
def pause(self):
|
||||
self.set_torrent_id(None)
|
||||
|
||||
@overrides(BaseMode)
|
||||
def on_resize(self, rows, cols):
|
||||
BaseMode.on_resize(self, rows, cols)
|
||||
self.__update_columns()
|
||||
if self.popup:
|
||||
self.popup.handle_resize()
|
||||
|
||||
self._listing_start = self.rows // 2
|
||||
self.refresh()
|
||||
|
||||
def set_state(self, state):
|
||||
log.debug("got state")
|
||||
|
||||
if state.get("files"):
|
||||
self.full_names = dict([(x["index"], x["path"]) for x in state["files"]])
|
||||
@ -130,11 +150,12 @@ class TorrentDetail(BaseMode, component.Component):
|
||||
("Files (torrent has %d files)" % len(state["files"])).center(self.cols))
|
||||
self.file_list, self.file_dict = self.build_file_list(state["files"], state["file_progress"],
|
||||
state["file_priorities"])
|
||||
self._status_keys.remove("files")
|
||||
else:
|
||||
self.files_sep = "{!green,black,bold,underline!}%s" % (("Files (File list unknown)").center(self.cols))
|
||||
need_prio_update = True
|
||||
|
||||
self.__fill_progress(self.file_list, state["file_progress"])
|
||||
|
||||
for i, prio in enumerate(state["file_priorities"]):
|
||||
if self.file_dict[i][6] != prio:
|
||||
need_prio_update = True
|
||||
@ -170,7 +191,7 @@ class TorrentDetail(BaseMode, component.Component):
|
||||
if not cur or path != cur[-1][0]:
|
||||
child_list = []
|
||||
if path == paths[-1]:
|
||||
file_progress = format_utils.format_progress(progress[torrent_file["index"]] * 100)
|
||||
file_progress = format_progress(progress[torrent_file["index"]] * 100)
|
||||
entry = [path, torrent_file["index"], torrent_file["size"], child_list,
|
||||
False, file_progress, priority[torrent_file["index"]]]
|
||||
file_dict[torrent_file["index"]] = entry
|
||||
@ -184,6 +205,7 @@ class TorrentDetail(BaseMode, component.Component):
|
||||
cur = cur[-1][3]
|
||||
self.__build_sizes(file_list)
|
||||
self.__fill_progress(file_list, progress)
|
||||
|
||||
return file_list, file_dict
|
||||
|
||||
# fill in the sizes of the directory entries based on their children
|
||||
@ -207,10 +229,10 @@ class TorrentDetail(BaseMode, component.Component):
|
||||
for f in fs:
|
||||
if f[3]: # dir, has some children
|
||||
bd = self.__fill_progress(f[3], progs)
|
||||
f[5] = format_utils.format_progress(bd / f[2] * 100)
|
||||
f[5] = format_progress(bd // f[2] * 100)
|
||||
else: # file, update own prog and add to total
|
||||
bd = f[2] * progs[f[1]]
|
||||
f[5] = format_utils.format_progress(progs[f[1]] * 100)
|
||||
f[5] = format_progress(progs[f[1]] * 100)
|
||||
tb += bd
|
||||
return tb
|
||||
|
||||
@ -242,16 +264,6 @@ class TorrentDetail(BaseMode, component.Component):
|
||||
self.column_string = "{!green,black,bold!}%s" % ("".join(["%s%s" % (self.column_names[i], " " * (
|
||||
self.column_widths[i] - len(self.column_names[i]))) for i in range(0, len(self.column_names))]))
|
||||
|
||||
def report_message(self, title, message):
|
||||
self.messages.append((title, message))
|
||||
|
||||
def clear_marks(self):
|
||||
self.marked = {}
|
||||
|
||||
def set_popup(self, pu):
|
||||
self.popup = pu
|
||||
self.refresh()
|
||||
|
||||
def _on_torrentremoved_event(self, torrent_id):
|
||||
if torrent_id == self.torrentid:
|
||||
self.back_to_overview()
|
||||
@ -343,10 +355,10 @@ class TorrentDetail(BaseMode, component.Component):
|
||||
else: # file
|
||||
xchar = "-"
|
||||
|
||||
r = format_utils.format_row(["%s%s %s" % (" " * depth, xchar, fl[0]),
|
||||
fsize(fl[2]), fl[5],
|
||||
format_utils.format_priority(fl[6])],
|
||||
self.column_widths)
|
||||
r = format_row(["%s%s %s" % (" " * depth, xchar, fl[0]),
|
||||
fsize(fl[2]), fl[5],
|
||||
format_priority(fl[6])],
|
||||
self.column_widths)
|
||||
|
||||
self.add_string(off, "%s%s" % (color_string, r), trim=False)
|
||||
off += 1
|
||||
@ -395,153 +407,109 @@ class TorrentDetail(BaseMode, component.Component):
|
||||
length += self.__get_contained_files_count(element[3])
|
||||
return length
|
||||
|
||||
def on_resize(self, *args):
|
||||
BaseMode.on_resize_norefresh(self, *args)
|
||||
|
||||
# Always refresh Legacy(it will also refresh AllTorrents), otherwise it will bug deluge out
|
||||
legacy = component.get("LegacyUI")
|
||||
legacy.on_resize(*args)
|
||||
|
||||
self.__update_columns()
|
||||
if self.popup:
|
||||
self.popup.handle_resize()
|
||||
|
||||
self._listing_start = self.rows // 2
|
||||
self.refresh()
|
||||
|
||||
def render_header(self, off):
|
||||
def render_header(self, row):
|
||||
status = self.torrent_state
|
||||
|
||||
up_color = colors.state_color["Seeding"]
|
||||
down_color = colors.state_color["Downloading"]
|
||||
download_color = "{!info!}"
|
||||
if status["download_payload_rate"] > 0:
|
||||
download_color = colors.state_color["Downloading"]
|
||||
|
||||
def add_field(name, row, pre_color="{!info!}", post_color="{!input!}"):
|
||||
s = "%s%s: %s%s" % (pre_color, torrent_data_fields[name]["name"],
|
||||
post_color, get_column_value(name, status))
|
||||
if row:
|
||||
row = self.add_string(row, s)
|
||||
return row
|
||||
return s
|
||||
|
||||
# Name
|
||||
s = "{!info!}Name: {!input!}%s" % status["name"]
|
||||
self.add_string(off, s)
|
||||
off += 1
|
||||
row = add_field("name", row)
|
||||
# State
|
||||
row = add_field("state", row)
|
||||
|
||||
# Print DL info and ETA
|
||||
if status["download_payload_rate"] > 0:
|
||||
s = "%sDownloading: {!input!}" % down_color
|
||||
else:
|
||||
s = "{!info!}Downloaded: {!input!}"
|
||||
s += fsize(status["all_time_download"])
|
||||
s = add_field("downloaded", 0, download_color)
|
||||
if status["progress"] != 100.0:
|
||||
s += "/%s" % fsize(status["total_wanted"])
|
||||
if status["download_payload_rate"] > 0:
|
||||
s += " {!yellow!}@ %s%s" % (down_color, fsize(status["download_payload_rate"]))
|
||||
s += "{!info!} ETA: {!input!}%s" % format_utils.format_time(status["eta"])
|
||||
self.add_string(off, s)
|
||||
off += 1
|
||||
s += " {!yellow!}@ %s%s" % (download_color, fsize(status["download_payload_rate"]))
|
||||
s += add_field("eta", 0)
|
||||
if s:
|
||||
row = self.add_string(row, s)
|
||||
|
||||
# Print UL info and ratio
|
||||
s = add_field("uploaded", 0, download_color)
|
||||
if status["upload_payload_rate"] > 0:
|
||||
s = "%sUploading: {!input!}" % up_color
|
||||
else:
|
||||
s = "{!info!}Uploaded: {!input!}"
|
||||
s += fsize(status["total_uploaded"])
|
||||
if status["upload_payload_rate"] > 0:
|
||||
s += " {!yellow!}@ %s%s" % (up_color, fsize(status["upload_payload_rate"]))
|
||||
ratio_str = format_utils.format_float(status["ratio"])
|
||||
if ratio_str == "-":
|
||||
ratio_str = "inf"
|
||||
s += " {!info!}Ratio: {!input!}%s" % ratio_str
|
||||
self.add_string(off, s)
|
||||
off += 1
|
||||
s += " {!yellow!}@ %s%s" % (colors.state_color["Seeding"], fsize(status["upload_payload_rate"]))
|
||||
s += " " + add_field("ratio", 0)
|
||||
row = self.add_string(row, s)
|
||||
|
||||
# Seed/peer info
|
||||
s = "{!info!}Seeds:{!green!} %s {!input!}(%s)" % (status["num_seeds"], status["total_seeds"])
|
||||
self.add_string(off, s)
|
||||
off += 1
|
||||
s = "{!info!}Peers:{!red!} %s {!input!}(%s)" % (status["num_peers"], status["total_peers"])
|
||||
self.add_string(off, s)
|
||||
off += 1
|
||||
s = "{!info!}%s:{!green!} %s {!input!}(%s)" % (torrent_data_fields["seeds"]["name"],
|
||||
status["num_seeds"], status["total_seeds"])
|
||||
row = self.add_string(row, s)
|
||||
s = "{!info!}%s:{!red!} %s {!input!}(%s)" % (torrent_data_fields["peers"]["name"],
|
||||
status["num_peers"], status["total_peers"])
|
||||
row = self.add_string(row, s)
|
||||
|
||||
# Tracker
|
||||
if status["message"] == "OK":
|
||||
color = "{!green!}"
|
||||
else:
|
||||
color = "{!red!}"
|
||||
s = "{!info!}Tracker: {!magenta!}%s{!input!} says \"%s%s{!input!}\"" % (
|
||||
status["tracker_host"], color, status["message"])
|
||||
self.add_string(off, s)
|
||||
off += 1
|
||||
tracker_color = "{!green!}" if status["message"] == "OK" else "{!red!}"
|
||||
s = "{!info!}%s: {!magenta!}%s{!input!} says \"%s%s{!input!}\"" % (
|
||||
torrent_data_fields["tracker"]["name"], status["tracker_host"], tracker_color, status["message"])
|
||||
row = self.add_string(row, s)
|
||||
|
||||
# Pieces and availability
|
||||
s = "{!info!}Pieces: {!yellow!}%s {!input!}x {!yellow!}%s" % (
|
||||
status["num_pieces"], fsize(status["piece_length"]))
|
||||
s = "{!info!}%s: {!yellow!}%s {!input!}x {!yellow!}%s" % (
|
||||
torrent_data_fields["pieces"]["name"], status["num_pieces"], fsize(status["piece_length"]))
|
||||
if status["distributed_copies"]:
|
||||
s += " {!info!}Availability: {!input!}%s" % format_utils.format_float(status["distributed_copies"])
|
||||
self.add_string(off, s)
|
||||
off += 1
|
||||
s += "{!info!}%s: {!input!}%s" % (torrent_data_fields["seed_rank"]["name"], status["seed_rank"])
|
||||
row = self.add_string(row, s)
|
||||
|
||||
# Time added
|
||||
s = "{!info!}Added: {!input!}%s" % fdate(status["time_added"])
|
||||
self.add_string(off, s)
|
||||
off += 1
|
||||
|
||||
row = add_field("time_added", row)
|
||||
# Time active
|
||||
s = "{!info!}Time active: {!input!}%s" % (ftime(status["active_time"]))
|
||||
row = add_field("active_time", row)
|
||||
if status["seeding_time"]:
|
||||
s += ", {!cyan!}%s{!input!} seeding" % (ftime(status["seeding_time"]))
|
||||
self.add_string(off, s)
|
||||
off += 1
|
||||
|
||||
row = add_field("seeding_time", row)
|
||||
# Download Folder
|
||||
s = "{!info!}Download Folder: {!input!}%s" % status["download_location"]
|
||||
self.add_string(off, s)
|
||||
off += 1
|
||||
|
||||
row = add_field("download_location", row)
|
||||
# Seed Rank
|
||||
row = add_field("seed_rank", row)
|
||||
# Last seen complete
|
||||
row = add_field("last_seen_complete", row)
|
||||
# Owner
|
||||
if status["owner"]:
|
||||
s = "{!info!}Owner: {!input!}%s" % status["owner"]
|
||||
|
||||
return off
|
||||
row = add_field("owner", row)
|
||||
return row
|
||||
|
||||
@overrides(BaseMode)
|
||||
def refresh(self, lines=None):
|
||||
# show a message popup if there's anything queued
|
||||
if self.popup is None and self.messages:
|
||||
title, msg = self.messages.popleft()
|
||||
self.popup = MessagePopup(self, title, msg)
|
||||
|
||||
# Update the status bars
|
||||
self.stdscr.erase()
|
||||
self.add_string(0, self.statusbars.topbar)
|
||||
self.draw_statusbars()
|
||||
|
||||
# This will quite likely fail when switching modes
|
||||
try:
|
||||
rf = format_utils.remove_formatting
|
||||
string = self.statusbars.bottombar
|
||||
hstr = "Press {!magenta,blue,bold!}[h]{!status!} for help"
|
||||
|
||||
string += " " * (self.cols - len(rf(string)) - len(rf(hstr))) + hstr
|
||||
|
||||
self.add_string(self.rows - 1, string)
|
||||
except Exception as ex:
|
||||
log.debug("Exception caught: %s", ex)
|
||||
|
||||
off = 1
|
||||
row = 1
|
||||
if self.torrent_state:
|
||||
off = self.render_header(off)
|
||||
row = self.render_header(row)
|
||||
else:
|
||||
self.add_string(1, "Waiting for torrent state")
|
||||
|
||||
off += 1
|
||||
row += 1
|
||||
|
||||
if self.files_sep:
|
||||
self.add_string(off, self.files_sep)
|
||||
off += 1
|
||||
self.add_string(row, self.files_sep)
|
||||
row += 1
|
||||
|
||||
self._listing_start = off
|
||||
self._listing_start = row
|
||||
self._listing_space = self.rows - self._listing_start
|
||||
|
||||
self.add_string(off, self.column_string)
|
||||
self.add_string(row, self.column_string)
|
||||
if self.file_list:
|
||||
off += 1
|
||||
row += 1
|
||||
self.more_to_draw = False
|
||||
self.draw_files(self.file_list, 0, off, 0)
|
||||
self.draw_files(self.file_list, 0, row, 0)
|
||||
|
||||
if component.get("ConsoleUI").screen != self:
|
||||
if not component.get("ConsoleUI").is_active_mode(self):
|
||||
return
|
||||
|
||||
self.stdscr.noutrefresh()
|
||||
@ -576,14 +544,6 @@ class TorrentDetail(BaseMode, component.Component):
|
||||
self.file_off = min(self.file_off, self.current_file_idx)
|
||||
self.refresh()
|
||||
|
||||
def back_to_overview(self):
|
||||
component.stop(["TorrentDetail"])
|
||||
component.deregister(self)
|
||||
self.stdscr.erase()
|
||||
component.get("ConsoleUI").set_mode(self.alltorrentmode)
|
||||
self.alltorrentmode._go_top = False
|
||||
self.alltorrentmode.resume()
|
||||
|
||||
# build list of priorities for all files in the torrent
|
||||
# based on what is currently selected and a selected priority.
|
||||
def build_prio_list(self, files, ret_list, parent_prio, selected_prio):
|
||||
@ -600,13 +560,11 @@ class TorrentDetail(BaseMode, component.Component):
|
||||
# not selected, just keep old priority
|
||||
ret_list.append((f[1], f[6]))
|
||||
|
||||
def do_priority(self, idx, data, was_empty):
|
||||
def do_priority(self, name, data, was_empty):
|
||||
plist = []
|
||||
self.build_prio_list(self.file_list, plist, -1, data)
|
||||
plist.sort()
|
||||
priorities = [p[1] for p in plist]
|
||||
log.debug("priorities: %s", priorities)
|
||||
|
||||
client.core.set_torrent_file_priorities(self.torrentid, priorities)
|
||||
|
||||
if was_empty:
|
||||
@ -615,16 +573,23 @@ class TorrentDetail(BaseMode, component.Component):
|
||||
|
||||
# show popup for priority selections
|
||||
def show_priority_popup(self, was_empty):
|
||||
def popup_func(idx, data, we=was_empty):
|
||||
return self.do_priority(idx, data, we)
|
||||
|
||||
def popup_func(data, *args, **kwargs):
|
||||
if data is None:
|
||||
return
|
||||
return self.do_priority(data, kwargs[data], was_empty)
|
||||
|
||||
if self.marked:
|
||||
self.popup = SelectablePopup(self, "Set File Priority", popup_func)
|
||||
self.popup.add_line("_Do Not Download", data=FILE_PRIORITY["Do Not Download"], foreground="red")
|
||||
self.popup.add_line("_Normal Priority", data=FILE_PRIORITY["Normal Priority"])
|
||||
self.popup.add_line("_High Priority", data=FILE_PRIORITY["High Priority"], foreground="yellow")
|
||||
self.popup.add_line("H_ighest Priority", data=FILE_PRIORITY["Highest Priority"], foreground="green")
|
||||
self.popup._selected = 1
|
||||
popup = SelectablePopup(self, "Set File Priority", popup_func, border_off_north=1)
|
||||
popup.add_line("do_not_download", "_Do Not Download",
|
||||
cb_arg=FILE_PRIORITY["Do Not Download"], foreground="red")
|
||||
popup.add_line("normal_priority", "_Normal Priority", cb_arg=FILE_PRIORITY["Normal Priority"])
|
||||
popup.add_line("high_priority", "_High Priority",
|
||||
cb_arg=FILE_PRIORITY["High Priority"], foreground="yellow")
|
||||
popup.add_line("highest_priority", "H_ighest Priority",
|
||||
cb_arg=FILE_PRIORITY["Highest Priority"], foreground="green")
|
||||
popup._selected = 1
|
||||
self.push_popup(popup)
|
||||
|
||||
def __mark_unmark(self, idx):
|
||||
"""
|
||||
@ -680,23 +645,19 @@ class TorrentDetail(BaseMode, component.Component):
|
||||
for element in file_list:
|
||||
if idx == num:
|
||||
return element
|
||||
|
||||
if element[3] and element[4]:
|
||||
i = self.__get_file_by_num(num, element[3], idx + 1)
|
||||
if not isinstance(i, int):
|
||||
return i
|
||||
else:
|
||||
idx = i
|
||||
idx = i
|
||||
else:
|
||||
idx += 1
|
||||
|
||||
return idx
|
||||
|
||||
def __get_file_by_name(self, name, file_list, idx=0):
|
||||
for element in file_list:
|
||||
if element[0].strip("/") == name.strip("/"):
|
||||
return element
|
||||
|
||||
if element[3] and element[4]:
|
||||
i = self.__get_file_by_name(name, element[3], idx + 1)
|
||||
if not isinstance(i, int):
|
||||
@ -705,7 +666,6 @@ class TorrentDetail(BaseMode, component.Component):
|
||||
idx = i
|
||||
else:
|
||||
idx += 1
|
||||
|
||||
return idx
|
||||
|
||||
def __unmark_tree(self, file_list, idx, unmark_all=False):
|
||||
@ -762,7 +722,7 @@ class TorrentDetail(BaseMode, component.Component):
|
||||
idx, true_idx = i
|
||||
else:
|
||||
idx += 1
|
||||
_, true_idx = i
|
||||
tmp, true_idx = i
|
||||
else:
|
||||
return i
|
||||
else:
|
||||
@ -780,19 +740,15 @@ class TorrentDetail(BaseMode, component.Component):
|
||||
if not element[3]:
|
||||
idx += 1
|
||||
continue
|
||||
|
||||
if num == idx:
|
||||
return "%s%s/" % (path, element[0])
|
||||
|
||||
if element[4]:
|
||||
i = self._get_full_folder_path(num, element[3], path + element[0] + "/", idx + 1)
|
||||
if not isinstance(i, int):
|
||||
return i
|
||||
else:
|
||||
idx = i
|
||||
idx = i
|
||||
else:
|
||||
idx += 1
|
||||
|
||||
return idx
|
||||
|
||||
def _do_rename_folder(self, torrent_id, folder, new_folder):
|
||||
@ -806,70 +762,55 @@ class TorrentDetail(BaseMode, component.Component):
|
||||
def _show_rename_popup(self):
|
||||
# Perhaps in the future: Renaming multiple files
|
||||
if self.marked:
|
||||
title = "Error (Enter to close)"
|
||||
text = "Sorry, you can't rename multiple files, please clear selection with {!info!}'c'{!normal!} key"
|
||||
self.popup = MessagePopup(self, title, text)
|
||||
self.report_message("Error (Enter to close)",
|
||||
"Sorry, you can't rename multiple files, please clear "
|
||||
"selection with {!info!}'c'{!normal!} key")
|
||||
else:
|
||||
_file = self.__get_file_by_num(self.current_file_idx, self.file_list)
|
||||
old_filename = _file[0]
|
||||
|
||||
idx = self._selection_to_file_idx()
|
||||
tid = self.torrentid
|
||||
|
||||
if _file[3]:
|
||||
|
||||
def do_rename(result):
|
||||
if not result["new_foldername"]:
|
||||
def do_rename(result, **kwargs):
|
||||
if not result or not result["new_foldername"]["value"] or kwargs.get("close", False):
|
||||
self.popup.close(None, call_cb=False)
|
||||
return
|
||||
old_fname = self._get_full_folder_path(self.current_file_idx)
|
||||
new_fname = "%s/%s/" % (old_fname.strip("/").rpartition("/")[0], result["new_foldername"])
|
||||
new_fname = "%s/%s/" % (old_fname.strip("/").rpartition("/")[0], result["new_foldername"]["value"])
|
||||
self._do_rename_folder(tid, old_fname, new_fname)
|
||||
|
||||
popup = InputPopup(self, "Rename folder (Esc to cancel)", close_cb=do_rename)
|
||||
popup.add_text("{!info!}Renaming folder:{!input!}")
|
||||
popup.add_text(" * %s\n" % old_filename)
|
||||
popup.add_text_input("Enter new folder name:", "new_foldername", old_filename.strip("/"))
|
||||
|
||||
self.popup = popup
|
||||
popup.add_text_input("new_foldername", "Enter new folder name:", old_filename.strip("/"), complete=True)
|
||||
self.push_popup(popup)
|
||||
else:
|
||||
|
||||
def do_rename(result):
|
||||
fname = "%s/%s" % (self.full_names[idx].rpartition("/")[0], result["new_filename"])
|
||||
def do_rename(result, **kwargs):
|
||||
if not result or not result["new_filename"]["value"] or kwargs.get("close", False):
|
||||
self.popup.close(None, call_cb=False)
|
||||
return
|
||||
fname = "%s/%s" % (self.full_names[idx].rpartition("/")[0], result["new_filename"]["value"])
|
||||
self._do_rename_file(tid, idx, fname)
|
||||
|
||||
popup = InputPopup(self, "Rename file (Esc to cancel)", close_cb=do_rename)
|
||||
popup.add_text("{!info!}Renaming file:{!input!}")
|
||||
popup.add_text(" * %s\n" % old_filename)
|
||||
popup.add_text_input("Enter new filename:", "new_filename", old_filename)
|
||||
|
||||
self.popup = popup
|
||||
popup = InputPopup(self, " Rename file ", close_cb=do_rename)
|
||||
popup.add_text_input("new_filename", "Enter new filename:", old_filename, complete=True)
|
||||
self.push_popup(popup)
|
||||
|
||||
@overrides(BaseMode)
|
||||
def read_input(self):
|
||||
c = self.stdscr.getch()
|
||||
|
||||
if self.popup:
|
||||
if self.popup.handle_read(c):
|
||||
self.popup = None
|
||||
ret = self.popup.handle_read(c)
|
||||
if ret != util.ReadState.IGNORED and self.popup.closed():
|
||||
self.pop_popup()
|
||||
self.refresh()
|
||||
return
|
||||
|
||||
if c > 31 and c < 256:
|
||||
if chr(c) == "Q":
|
||||
from twisted.internet import reactor
|
||||
if client.connected():
|
||||
def on_disconnect(result):
|
||||
reactor.stop()
|
||||
client.disconnect().addCallback(on_disconnect)
|
||||
else:
|
||||
reactor.stop()
|
||||
return
|
||||
elif chr(c) == "q":
|
||||
self.back_to_overview()
|
||||
return
|
||||
|
||||
if c == 27 or c == curses.KEY_LEFT:
|
||||
if c in [util.KEY_ESC, curses.KEY_LEFT, ord('q')]:
|
||||
self.back_to_overview()
|
||||
return
|
||||
return util.ReadState.READ
|
||||
|
||||
if not self.torrent_state:
|
||||
# actions below only make sense if there is a torrent state
|
||||
@ -892,35 +833,30 @@ class TorrentDetail(BaseMode, component.Component):
|
||||
self.file_off = self.current_file_idx - (self._listing_space - 3)
|
||||
elif c == curses.KEY_DC:
|
||||
torrent_actions_popup(self, [self.torrentid], action=ACTION.REMOVE)
|
||||
# Enter Key
|
||||
elif c == curses.KEY_ENTER or c == 10:
|
||||
elif c in [curses.KEY_ENTER, util.KEY_ENTER2]:
|
||||
was_empty = (self.marked == {})
|
||||
self.__mark_tree(self.file_list, self.current_file[1])
|
||||
self.show_priority_popup(was_empty)
|
||||
|
||||
# space
|
||||
elif c == 32:
|
||||
elif c == util.KEY_SPACE:
|
||||
self.expcol_cur_file()
|
||||
else:
|
||||
if c > 31 and c < 256:
|
||||
if chr(c) == "m":
|
||||
if self.current_file:
|
||||
self.__mark_unmark(self.current_file[1])
|
||||
elif chr(c) == "r":
|
||||
self._show_rename_popup()
|
||||
elif chr(c) == "c":
|
||||
self.marked = {}
|
||||
elif chr(c) == "a":
|
||||
torrent_actions_popup(self, [self.torrentid], details=False)
|
||||
return
|
||||
elif chr(c) == "o":
|
||||
torrent_actions_popup(self, [self.torrentid], action=ACTION.TORRENT_OPTIONS)
|
||||
return
|
||||
elif chr(c) == "h":
|
||||
self.popup = MessagePopup(self, "Help", HELP_STR, width_req=0.75)
|
||||
elif chr(c) == "j":
|
||||
self.file_list_up()
|
||||
if chr(c) == "k":
|
||||
self.file_list_down()
|
||||
elif c == ord('m'):
|
||||
if self.current_file:
|
||||
self.__mark_unmark(self.current_file[1])
|
||||
elif c == ord('r'):
|
||||
self._show_rename_popup()
|
||||
elif c == ord('c'):
|
||||
self.marked = {}
|
||||
elif c == ord('a'):
|
||||
torrent_actions_popup(self, [self.torrentid], details=False)
|
||||
return
|
||||
elif c == ord('o'):
|
||||
torrent_actions_popup(self, [self.torrentid], action=ACTION.TORRENT_OPTIONS)
|
||||
return
|
||||
elif c == ord('h'):
|
||||
self.push_popup(MessagePopup(self, "Help", HELP_STR, width_req=0.75))
|
||||
elif c == ord('j'):
|
||||
self.file_list_up()
|
||||
elif c == ord('k'):
|
||||
self.file_list_down()
|
||||
|
||||
self.refresh()
|
||||
|
18
deluge/ui/console/modes/torrentlist/__init__.py
Normal file
18
deluge/ui/console/modes/torrentlist/__init__.py
Normal file
@ -0,0 +1,18 @@
|
||||
|
||||
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"
|
87
deluge/ui/console/modes/torrentlist/add_torrents_popup.py
Normal file
87
deluge/ui/console/modes/torrentlist/add_torrents_popup.py
Normal file
@ -0,0 +1,87 @@
|
||||
# -*- 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.
|
||||
#
|
||||
|
||||
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)
|
106
deluge/ui/console/modes/torrentlist/filtersidebar.py
Normal file
106
deluge/ui/console/modes/torrentlist/filtersidebar.py
Normal file
@ -0,0 +1,106 @@
|
||||
# -*- 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.
|
||||
#
|
||||
|
||||
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()
|
115
deluge/ui/console/modes/torrentlist/queue_mode.py
Normal file
115
deluge/ui/console/modes/torrentlist/queue_mode.py
Normal file
@ -0,0 +1,115 @@
|
||||
# -*- 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.
|
||||
#
|
||||
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
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
|
||||
|
||||
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 = 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 = 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)
|
199
deluge/ui/console/modes/torrentlist/search_mode.py
Normal file
199
deluge/ui/console/modes/torrentlist/search_mode.py
Normal file
@ -0,0 +1,199 @@
|
||||
# -*- 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.
|
||||
#
|
||||
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import logging
|
||||
|
||||
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
|
||||
|
||||
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):
|
||||
InputKeyHandler.__init__(self)
|
||||
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 = ""
|
||||
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
|
241
deluge/ui/console/modes/torrentlist/torrentactions.py
Normal file
241
deluge/ui/console/modes/torrentlist/torrentactions.py
Normal file
@ -0,0 +1,241 @@
|
||||
# -*- 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.
|
||||
#
|
||||
|
||||
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.widgets.popup import InputPopup, MessagePopup, SelectablePopup
|
||||
|
||||
from . import ACTION
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
torrent_options = [
|
||||
"max_download_speed", "max_upload_speed", "max_connections", "max_upload_slots",
|
||||
"prioritize_first_last", "sequential_download", "is_auto_managed", "stop_at_ratio",
|
||||
"stop_ratio", "remove_at_ratio", "move_completed", "move_completed_path"]
|
||||
|
||||
|
||||
def action_error(error, mode):
|
||||
rerr = error.value
|
||||
mode.report_message("An Error Occurred", "%s got error %s: %s" % (
|
||||
rerr.method, rerr.exception_type, rerr.exception_msg))
|
||||
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.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)
|
||||
|
||||
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.iteritems():
|
||||
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]
|
||||
field_type = type(value)
|
||||
if field_type in [str, unicode]:
|
||||
if not isinstance(value, basestring):
|
||||
value = str(value)
|
||||
option_popup.add_text_input(field, caption, value)
|
||||
elif field_type == bool:
|
||||
choices = (["Yes", "No"], [True, False], [True, False].index(options[field]))
|
||||
option_popup.add_select_input(field, caption, choices[0], choices[1], choices[2])
|
||||
elif field_type == float:
|
||||
option_popup.add_float_spin_input(field, caption, value=value, min_val=-1)
|
||||
elif field_type == 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, 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_torrent(torrent_ids).addErrback(action_error, mode)
|
||||
retval = True
|
||||
elif action == ACTION.RESUME:
|
||||
log.debug("Resuming torrents: %s", torrent_ids)
|
||||
client.core.resume_torrent(torrent_ids).addErrback(action_error, mode)
|
||||
retval = True
|
||||
elif action == ACTION.QUEUE:
|
||||
queue_mode = QueueMode(mode, torrent_ids)
|
||||
queue_mode.popup(**kwargs)
|
||||
return False
|
||||
elif action == ACTION.REMOVE:
|
||||
action_remove(**kwargs)
|
||||
return False
|
||||
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)
|
335
deluge/ui/console/modes/torrentlist/torrentlist.py
Normal file
335
deluge/ui/console/modes/torrentlist/torrentlist.py
Normal file
@ -0,0 +1,335 @@
|
||||
# -*- 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.
|
||||
#
|
||||
|
||||
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_char(c):
|
||||
if chr(c) == "Q":
|
||||
from twisted.internet import reactor
|
||||
if client.connected():
|
||||
def on_disconnect(result):
|
||||
reactor.stop()
|
||||
client.disconnect().addCallback(on_disconnect)
|
||||
else:
|
||||
reactor.stop()
|
||||
return
|
||||
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_char(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)
|
481
deluge/ui/console/modes/torrentlist/torrentview.py
Normal file
481
deluge/ui/console/modes/torrentlist/torrentview.py
Normal file
@ -0,0 +1,481 @@
|
||||
|
||||
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"}
|
||||
|
||||
|
||||
def _queue_sort(v1, v2):
|
||||
if v1 == v2:
|
||||
return 0
|
||||
if v2 < 0:
|
||||
return -1
|
||||
if v1 < 0:
|
||||
return 1
|
||||
if v1 > v2:
|
||||
return 1
|
||||
if v2 > v1:
|
||||
return -1
|
||||
|
||||
|
||||
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):
|
||||
InputKeyHandler.__init__(self)
|
||||
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[state.keys()[0]]
|
||||
if field in first_element:
|
||||
is_string = isinstance(first_element[field], basestring)
|
||||
|
||||
def sort_key(s):
|
||||
return state.get(s)[field]
|
||||
|
||||
def sort_key2(s):
|
||||
return state.get(s)[field].lower()
|
||||
|
||||
# If it's a string, sort case-insensitively but preserve A>a order
|
||||
to_sort = sorted(to_sort, _queue_sort, sort_key, reverse)
|
||||
if is_string:
|
||||
to_sort = sorted(to_sort, _queue_sort, sort_key2, 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, _queue_sort, 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 l in lines:
|
||||
if l < tidx:
|
||||
continue
|
||||
if l >= (tidx + self.torrent_rows) or l >= self.numtorrents:
|
||||
break
|
||||
todraw.append((l, l - self.curoff, draw_row(l)))
|
||||
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 = range(self.last_mark, self.cursel + 1)
|
||||
else:
|
||||
mrange = 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
|
102
deluge/ui/console/modes/torrentlist/torrentviewcolumns.py
Normal file
102
deluge/ui/console/modes/torrentlist/torrentviewcolumns.py
Normal file
@ -0,0 +1,102 @@
|
||||
# -*- 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 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",
|
||||
"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)
|
145
deluge/ui/console/parser.py
Normal file
145
deluge/ui/console/parser.py
Normal file
@ -0,0 +1,145 @@
|
||||
# -*- 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 print_function
|
||||
|
||||
import argparse
|
||||
import shlex
|
||||
|
||||
import deluge.component as component
|
||||
from deluge.ui.console.utils.colors import ConsoleColorFormatter
|
||||
|
||||
|
||||
class OptionParserError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ConsoleBaseParser(argparse.ArgumentParser):
|
||||
|
||||
def format_help(self):
|
||||
"""Differs from ArgumentParser.format_help by adding the raw epilog
|
||||
as formatted in the string. Default bahavior mangles the formatting.
|
||||
|
||||
"""
|
||||
# Handle epilog manually to keep the text formatting
|
||||
epilog = self.epilog
|
||||
self.epilog = ""
|
||||
help_str = super(ConsoleBaseParser, self).format_help()
|
||||
if epilog is not None:
|
||||
help_str += epilog
|
||||
self.epilog = epilog
|
||||
return help_str
|
||||
|
||||
|
||||
class ConsoleCommandParser(ConsoleBaseParser):
|
||||
|
||||
def _split_args(self, args):
|
||||
command_options = []
|
||||
for a in args:
|
||||
if not a:
|
||||
continue
|
||||
if ";" in a:
|
||||
cmd_lines = [arg.strip() for arg in a.split(";")]
|
||||
elif " " in a:
|
||||
cmd_lines = [a]
|
||||
else:
|
||||
continue
|
||||
|
||||
for cmd_line in cmd_lines:
|
||||
cmds = shlex.split(cmd_line)
|
||||
cmd_options = super(ConsoleCommandParser, self).parse_args(args=cmds)
|
||||
cmd_options.command = cmds[0]
|
||||
command_options.append(cmd_options)
|
||||
|
||||
return command_options
|
||||
|
||||
def parse_args(self, args=None):
|
||||
"""Parse known UI args and handle common and process group options.
|
||||
|
||||
Notes:
|
||||
If started by deluge entry script this has already been done.
|
||||
|
||||
Args:
|
||||
args (list, optional): The arguments to parse.
|
||||
|
||||
Returns:
|
||||
argparse.Namespace: The parsed arguments.
|
||||
"""
|
||||
from deluge.ui.ui_entry import AMBIGUOUS_CMD_ARGS
|
||||
self.base_parser.parse_known_ui_args(args, withhold=AMBIGUOUS_CMD_ARGS)
|
||||
|
||||
multi_command = self._split_args(args)
|
||||
# If multiple commands were passed to console
|
||||
if multi_command:
|
||||
# With multiple commands, normal parsing will fail, so only parse
|
||||
# known arguments using the base parser, and then set
|
||||
# options.parsed_cmds to the already parsed commands
|
||||
options, remaining = self.base_parser.parse_known_args(args=args)
|
||||
options.parsed_cmds = multi_command
|
||||
else:
|
||||
subcommand = False
|
||||
if hasattr(self.base_parser, "subcommand"):
|
||||
subcommand = getattr(self.base_parser, "subcommand")
|
||||
if not subcommand:
|
||||
# We must use parse_known_args to handle case when no subcommand
|
||||
# is provided, because argparse does not support parsing without
|
||||
# a subcommand
|
||||
options, remaining = self.base_parser.parse_known_args(args=args)
|
||||
# If any options remain it means they do not exist. Reparse with
|
||||
# parse_args to trigger help message
|
||||
if remaining:
|
||||
options = self.base_parser.parse_args(args=args)
|
||||
options.parsed_cmds = []
|
||||
else:
|
||||
options = super(ConsoleCommandParser, self).parse_args(args=args)
|
||||
options.parsed_cmds = [options]
|
||||
|
||||
if not hasattr(options, "remaining"):
|
||||
options.remaining = []
|
||||
|
||||
return options
|
||||
|
||||
|
||||
class OptionParser(ConsoleBaseParser):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(OptionParser, self).__init__(**kwargs)
|
||||
self.formatter = ConsoleColorFormatter()
|
||||
|
||||
def exit(self, status=0, msg=None):
|
||||
self._exit = True
|
||||
if msg:
|
||||
print(msg)
|
||||
|
||||
def error(self, msg):
|
||||
"""error(msg : string)
|
||||
|
||||
Print a usage message incorporating 'msg' to stderr and exit.
|
||||
If you override this in a subclass, it should not return -- it
|
||||
should either exit or raise an exception.
|
||||
"""
|
||||
raise OptionParserError(msg)
|
||||
|
||||
def print_usage(self, _file=None):
|
||||
console = component.get("ConsoleUI")
|
||||
if self.usage:
|
||||
for line in self.format_usage().splitlines():
|
||||
console.write(line)
|
||||
|
||||
def print_help(self, _file=None):
|
||||
console = component.get("ConsoleUI")
|
||||
console.set_batch_write(True)
|
||||
for line in self.format_help().splitlines():
|
||||
console.write(line)
|
||||
console.set_batch_write(False)
|
||||
|
||||
def format_help(self):
|
||||
"""Return help formatted with colors."""
|
||||
help_str = super(OptionParser, self).format_help()
|
||||
return self.formatter.format_colors(help_str)
|
0
deluge/ui/console/utils/__init__.py
Normal file
0
deluge/ui/console/utils/__init__.py
Normal file
@ -7,15 +7,18 @@
|
||||
# See LICENSE for more details.
|
||||
#
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
from deluge.ui.console.modes import format_utils
|
||||
from deluge.ui.console.utils import format_utils
|
||||
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
colors = [
|
||||
"COLOR_BLACK",
|
||||
"COLOR_BLUE",
|
||||
@ -69,24 +72,34 @@ type_color = {
|
||||
}
|
||||
|
||||
|
||||
def get_color_pair(fg, bg):
|
||||
return color_pairs[(fg, bg)]
|
||||
|
||||
|
||||
def init_colors():
|
||||
curses.start_color()
|
||||
|
||||
# We want to redefine white/black as it makes underlining work for some terminals
|
||||
# but can also fail on others, so we try/except
|
||||
|
||||
def define_pair(counter, fg_name, bg_name, fg, bg):
|
||||
try:
|
||||
curses.init_pair(counter, fg, bg)
|
||||
color_pairs[(fg_name, bg_name)] = counter
|
||||
counter += 1
|
||||
except curses.error as ex:
|
||||
log.warn("Error: %s", ex)
|
||||
return counter
|
||||
|
||||
# Create the color_pairs dict
|
||||
counter = 1
|
||||
for fg in colors:
|
||||
for bg in colors:
|
||||
if fg == "COLOR_WHITE" and bg == "COLOR_BLACK":
|
||||
continue
|
||||
color_pairs[(fg[6:].lower(), bg[6:].lower())] = counter
|
||||
curses.init_pair(counter, getattr(curses, fg), getattr(curses, bg))
|
||||
counter += 1
|
||||
counter = define_pair(counter, fg[6:].lower(), bg[6:].lower(), getattr(curses, fg), getattr(curses, bg))
|
||||
|
||||
# try to redefine white/black as it makes underlining work for some terminals
|
||||
# but can also fail on others, so we try/except
|
||||
try:
|
||||
curses.init_pair(counter, curses.COLOR_WHITE, curses.COLOR_BLACK)
|
||||
color_pairs[("white", "black")] = counter
|
||||
except Exception:
|
||||
pass
|
||||
counter = define_pair(counter, "white", "grey", curses.COLOR_WHITE, 241)
|
||||
counter = define_pair(counter, "black", "whitegrey", curses.COLOR_BLACK, 249)
|
||||
counter = define_pair(counter, "magentadark", "white", 99, curses.COLOR_WHITE)
|
||||
|
||||
|
||||
class BadColorString(Exception):
|
||||
@ -169,6 +182,7 @@ def parse_color_string(s, encoding="UTF-8"):
|
||||
s = s.encode(encoding, "replace")
|
||||
|
||||
ret = []
|
||||
last_color_attr = None
|
||||
# Keep track of where the strings
|
||||
while s.find("{!") != -1:
|
||||
begin = s.find("{!")
|
||||
@ -185,11 +199,19 @@ def parse_color_string(s, encoding="UTF-8"):
|
||||
if len(attrs) == 1 and not attrs[0].strip(" "):
|
||||
raise BadColorString("No description in {! !}")
|
||||
|
||||
def apply_attrs(cp, a):
|
||||
def apply_attrs(cp, attrs):
|
||||
# This function applies any additional attributes as necessary
|
||||
if len(a) > 2:
|
||||
for attr in a[2:]:
|
||||
for attr in attrs:
|
||||
if attr == "ignore":
|
||||
continue
|
||||
mode = '+'
|
||||
if attr[0] in ['+', '-']:
|
||||
mode = attr[0]
|
||||
attr = attr[1:]
|
||||
if mode == '+':
|
||||
cp |= getattr(curses, "A_" + attr.upper())
|
||||
else:
|
||||
cp ^= getattr(curses, "A_" + attr.upper())
|
||||
return cp
|
||||
|
||||
# Check for a builtin type first
|
||||
@ -203,35 +225,44 @@ def parse_color_string(s, encoding="UTF-8"):
|
||||
color_pair = apply_attrs(color_pair, schemes[attrs[0]][2:])
|
||||
last_color_attr = color_pair
|
||||
else:
|
||||
# This is a custom color scheme
|
||||
fg = attrs[0]
|
||||
bg = "black" # Default to 'black' if no bg is chosen
|
||||
if len(attrs) > 1:
|
||||
bg = attrs[1]
|
||||
try:
|
||||
pair = (fg, bg)
|
||||
if pair not in color_pairs:
|
||||
# Color pair missing, this could be because the
|
||||
# terminal settings allows no colors. If background is white, we
|
||||
# assume this means selection, and use "white", "black" + reverse
|
||||
# To have white background and black foreground
|
||||
log.debug("Pair doesn't exist: %s", pair)
|
||||
if pair[1] == "white":
|
||||
if "ignore" == attrs[2]:
|
||||
attrs[2] = "reverse"
|
||||
else:
|
||||
attrs.append("reverse")
|
||||
pair = ("white", "black")
|
||||
|
||||
color_pair = curses.color_pair(color_pairs[pair])
|
||||
last_color_attr = color_pair
|
||||
attrs = attrs[2:] # Remove colors
|
||||
except KeyError:
|
||||
raise BadColorString("Bad color value in tag: %s,%s" % (fg, bg))
|
||||
attrlist = ["blink", "bold", "dim", "reverse", "standout", "underline"]
|
||||
|
||||
if attrs[0][0] in ['+', '-']:
|
||||
# Color is not given, so use last color
|
||||
if last_color_attr is None:
|
||||
raise BadColorString("No color value given when no previous color was used!: %s" % (attrs[0]))
|
||||
color_pair = last_color_attr
|
||||
for i, attr in enumerate(attrs):
|
||||
if attr[1:] not in attrlist:
|
||||
raise BadColorString("Bad attribute value!: %s" % (attr))
|
||||
else:
|
||||
# This is a custom color scheme
|
||||
fg = attrs[0]
|
||||
bg = "black" # Default to 'black' if no bg is chosen
|
||||
if len(attrs) > 1:
|
||||
bg = attrs[1]
|
||||
try:
|
||||
pair = (fg, bg)
|
||||
if pair not in color_pairs:
|
||||
# Color pair missing, this could be because the
|
||||
# terminal settings allows no colors. If background is white, we
|
||||
# assume this means selection, and use "white", "black" + reverse
|
||||
# To have white background and black foreground
|
||||
log.debug("Color pair doesn't exist: %s", pair)
|
||||
if pair[1] == "white":
|
||||
if attrs[2] == "ignore":
|
||||
attrs[2] = "reverse"
|
||||
else:
|
||||
attrs.append("reverse")
|
||||
pair = ("white", "black")
|
||||
color_pair = curses.color_pair(color_pairs[pair])
|
||||
last_color_attr = color_pair
|
||||
attrs = attrs[2:] # Remove colors
|
||||
except KeyError:
|
||||
raise BadColorString("Bad color value in tag: %s,%s" % (fg, bg))
|
||||
# Check for additional attributes and OR them to the color_pair
|
||||
color_pair = apply_attrs(color_pair, attrs)
|
||||
|
||||
last_color_attr = color_pair
|
||||
# We need to find the text now, so lets try to find another {! and if
|
||||
# there isn't one, then it's the rest of the string
|
||||
next_begin = s.find("{!", end)
|
||||
@ -251,7 +282,7 @@ def parse_color_string(s, encoding="UTF-8"):
|
||||
|
||||
class ConsoleColorFormatter(object):
|
||||
"""
|
||||
Format help in a way suited to deluge Legacy mode - colors, format, indentation...
|
||||
Format help in a way suited to deluge CmdLine mode - colors, format, indentation...
|
||||
"""
|
||||
|
||||
replace_dict = {
|
92
deluge/ui/console/utils/column.py
Normal file
92
deluge/ui/console/utils/column.py
Normal file
@ -0,0 +1,92 @@
|
||||
# -*- 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.
|
||||
#
|
||||
|
||||
import copy
|
||||
import logging
|
||||
|
||||
import deluge.common
|
||||
from deluge.ui.common import TORRENT_DATA_FIELD
|
||||
from deluge.ui.util import lang
|
||||
|
||||
from . import format_utils
|
||||
|
||||
lang.setup_translations()
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
torrent_data_fields = copy.deepcopy(TORRENT_DATA_FIELD)
|
||||
|
||||
|
||||
def format_queue(qnum):
|
||||
if qnum < 0:
|
||||
return ""
|
||||
return "%d" % (qnum + 1)
|
||||
|
||||
|
||||
formatters = {
|
||||
"queue": format_queue,
|
||||
"name": lambda a, b: b,
|
||||
"state": None,
|
||||
"tracker": None,
|
||||
"download_location": None,
|
||||
"owner": None,
|
||||
|
||||
"progress_state": format_utils.format_progress,
|
||||
"progress": format_utils.format_progress,
|
||||
|
||||
"size": deluge.common.fsize,
|
||||
"downloaded": deluge.common.fsize,
|
||||
"uploaded": deluge.common.fsize,
|
||||
"remaining": deluge.common.fsize,
|
||||
|
||||
"ratio": format_utils.format_float,
|
||||
"avail": format_utils.format_float,
|
||||
"seeds_peers_ratio": format_utils.format_float,
|
||||
|
||||
"download_speed": format_utils.format_speed,
|
||||
"upload_speed": format_utils.format_speed,
|
||||
"max_download_speed": format_utils.format_speed,
|
||||
"max_upload_speed": format_utils.format_speed,
|
||||
|
||||
"peers": format_utils.format_seeds_peers,
|
||||
"seeds": format_utils.format_seeds_peers,
|
||||
|
||||
"time_added": deluge.common.fdate,
|
||||
"seeding_time": deluge.common.ftime,
|
||||
"active_time": deluge.common.ftime,
|
||||
"finished_time": deluge.common.ftime,
|
||||
|
||||
"last_seen_complete": format_utils.format_date_never,
|
||||
"completed_time": format_utils.format_date,
|
||||
"eta": format_utils.format_time,
|
||||
"pieces": format_utils.format_pieces,
|
||||
}
|
||||
|
||||
torrent_data_fields["pieces"] = {"name": _("Pieces"), "status": ["num_pieces", "piece_length"]}
|
||||
torrent_data_fields["seed_rank"] = {"name": _("Seed Rank"), "status": ["seed_rank"]}
|
||||
|
||||
for data_field in torrent_data_fields:
|
||||
torrent_data_fields[data_field]["formatter"] = formatters.get(data_field, str)
|
||||
|
||||
|
||||
def get_column_value(name, state):
|
||||
col = torrent_data_fields[name]
|
||||
|
||||
if col["formatter"]:
|
||||
args = [state[key] for key in col["status"]]
|
||||
return col["formatter"](*args)
|
||||
else:
|
||||
return state[col["status"][0]]
|
||||
|
||||
|
||||
def get_required_fields(cols):
|
||||
fields = []
|
||||
for col in cols:
|
||||
fields.extend(torrent_data_fields[col]["status"])
|
||||
return fields
|
63
deluge/ui/console/utils/curses_util.py
Normal file
63
deluge/ui/console/utils/curses_util.py
Normal file
@ -0,0 +1,63 @@
|
||||
# -*- 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.
|
||||
#
|
||||
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
KEY_BELL = 7 # CTRL-/ ^G (curses.keyname(KEY_BELL) == "^G")
|
||||
KEY_TAB = 9
|
||||
KEY_ENTER2 = 10
|
||||
KEY_ESC = 27
|
||||
KEY_SPACE = 32
|
||||
KEY_BACKSPACE2 = 127
|
||||
|
||||
KEY_ALT_AND_ARROW_UP = 564
|
||||
KEY_ALT_AND_ARROW_DOWN = 523
|
||||
|
||||
KEY_ALT_AND_KEY_PPAGE = 553
|
||||
KEY_ALT_AND_KEY_NPAGE = 548
|
||||
|
||||
KEY_CTRL_AND_ARROW_UP = 566
|
||||
KEY_CTRL_AND_ARROW_DOWN = 525
|
||||
|
||||
|
||||
def is_printable_char(c):
|
||||
return c >= 32 and c <= 126
|
||||
|
||||
|
||||
def is_int_chr(c):
|
||||
return c > 47 and c < 58
|
||||
|
||||
|
||||
class Curser(object):
|
||||
INVISIBLE = 0
|
||||
NORMAL = 1
|
||||
VERY_VISIBLE = 2
|
||||
|
||||
|
||||
def safe_curs_set(visibility):
|
||||
"""
|
||||
Args:
|
||||
visibility(int): 0, 1, or 2, for invisible, normal, or very visible
|
||||
|
||||
curses.curs_set fails on monochrome terminals so use this
|
||||
to ignore errors
|
||||
"""
|
||||
try:
|
||||
curses.curs_set(visibility)
|
||||
except curses.error:
|
||||
pass
|
||||
|
||||
|
||||
class ReadState(object):
|
||||
IGNORED = 0
|
||||
READ = 1
|
||||
CHANGED = 2
|
@ -123,6 +123,8 @@ def format_row(row, column_widths):
|
||||
|
||||
_strip_re = re.compile("\\{!.*?!\\}")
|
||||
|
||||
_format_code = re.compile(r"\{\|(.*)\|\}")
|
||||
|
||||
|
||||
def remove_formatting(string):
|
||||
return re.sub(_strip_re, "", string)
|
||||
@ -140,6 +142,7 @@ def wrap_string(string, width, min_lines=0, strip_colors=True):
|
||||
"""
|
||||
ret = []
|
||||
s1 = string.split("\n")
|
||||
indent = ""
|
||||
|
||||
def insert_clr(s, offset, mtchs, clrs):
|
||||
end_pos = offset + len(s)
|
||||
@ -152,6 +155,16 @@ def wrap_string(string, width, min_lines=0, strip_colors=True):
|
||||
|
||||
for s in s1:
|
||||
offset = 0
|
||||
indent = ""
|
||||
m = _format_code.search(remove_formatting(s))
|
||||
if m:
|
||||
if m.group(1).startswith("indent:"):
|
||||
indent = m.group(1)[len("indent:"):]
|
||||
elif m.group(1).startswith("indent_pos:"):
|
||||
begin = m.start(0)
|
||||
indent = " " * begin
|
||||
s = _format_code.sub("", s)
|
||||
|
||||
if strip_colors:
|
||||
mtchs = deque()
|
||||
clrs = deque()
|
||||
@ -161,17 +174,28 @@ def wrap_string(string, width, min_lines=0, strip_colors=True):
|
||||
cstr = _strip_re.sub("", s)
|
||||
else:
|
||||
cstr = s
|
||||
while len(cstr) > width:
|
||||
sidx = cstr.rfind(" ", 0, width - 1)
|
||||
|
||||
def append_indent(l, string, offset):
|
||||
"""Prepends indent to string if specified"""
|
||||
if indent and offset != 0:
|
||||
string = indent + string
|
||||
l.append(string)
|
||||
|
||||
while cstr:
|
||||
# max with for a line. If indent is specified, we account for this
|
||||
max_width = width - (len(indent) if offset != 0 else 0)
|
||||
if len(cstr) < max_width:
|
||||
break
|
||||
sidx = cstr.rfind(" ", 0, max_width - 1)
|
||||
sidx += 1
|
||||
if sidx > 0:
|
||||
if strip_colors:
|
||||
to_app = cstr[0:sidx]
|
||||
to_app = insert_clr(to_app, offset, mtchs, clrs)
|
||||
ret.append(to_app)
|
||||
append_indent(ret, to_app, offset)
|
||||
offset += len(to_app)
|
||||
else:
|
||||
ret.append(cstr[0:sidx])
|
||||
append_indent(ret, cstr[0:sidx], offset)
|
||||
cstr = cstr[sidx:]
|
||||
if not cstr:
|
||||
cstr = None
|
||||
@ -181,19 +205,19 @@ def wrap_string(string, width, min_lines=0, strip_colors=True):
|
||||
if strip_colors:
|
||||
to_app = cstr[0:width]
|
||||
to_app = insert_clr(to_app, offset, mtchs, clrs)
|
||||
ret.append(to_app)
|
||||
append_indent(ret, to_app, offset)
|
||||
offset += len(to_app)
|
||||
else:
|
||||
ret.append(cstr[0:width])
|
||||
append_indent(ret, cstr[0:width], offset)
|
||||
cstr = cstr[width:]
|
||||
if not cstr:
|
||||
cstr = None
|
||||
break
|
||||
if cstr is not None:
|
||||
to_append = cstr
|
||||
if strip_colors:
|
||||
ret.append(insert_clr(cstr, offset, mtchs, clrs))
|
||||
else:
|
||||
ret.append(cstr)
|
||||
to_append = insert_clr(cstr, offset, mtchs, clrs)
|
||||
append_indent(ret, to_append, offset)
|
||||
|
||||
if min_lines > 0:
|
||||
for i in range(len(ret), min_lines):
|
||||
@ -231,3 +255,38 @@ def pad_string(string, length, character=" ", side="right"):
|
||||
return "%s%s" % (character * diff, string)
|
||||
elif side == "right":
|
||||
return "%s%s" % (string, character * diff)
|
||||
|
||||
|
||||
def delete_alt_backspace(input_text, input_cursor, sep_chars=" *?!._~-#$^;'\"/"):
|
||||
"""
|
||||
Remove text from input_text on ALT+backspace
|
||||
Stop removing when countering any of the sep chars
|
||||
"""
|
||||
deleted = 0
|
||||
seg_start = input_text[:input_cursor]
|
||||
seg_end = input_text[input_cursor:]
|
||||
none_space_deleted = False # Track if any none-space characters have been deleted
|
||||
|
||||
while seg_start and input_cursor > 0:
|
||||
if (not seg_start) or (input_cursor == 0):
|
||||
break
|
||||
if deleted and seg_start[-1] in sep_chars:
|
||||
if seg_start[-1] == " ":
|
||||
if seg_start[-2] == " " or none_space_deleted is False:
|
||||
# Continue as long as:
|
||||
# * next char is also a space
|
||||
# * no none-space characters have been deleted
|
||||
pass
|
||||
else:
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
if not none_space_deleted:
|
||||
none_space_deleted = seg_start[-1] != " "
|
||||
seg_start = seg_start[:-1]
|
||||
deleted += 1
|
||||
input_cursor -= 1
|
||||
|
||||
input_text = seg_start + seg_end
|
||||
return input_text, input_cursor
|
3
deluge/ui/console/widgets/__init__.py
Normal file
3
deluge/ui/console/widgets/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from deluge.ui.console.widgets.inputpane import BaseInputPane # NOQA
|
||||
from deluge.ui.console.widgets.statusbars import StatusBars # NOQA
|
||||
from deluge.ui.console.widgets.window import BaseWindow # NOQA
|
969
deluge/ui/console/widgets/fields.py
Normal file
969
deluge/ui/console/widgets/fields.py
Normal file
@ -0,0 +1,969 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||
# 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.
|
||||
#
|
||||
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from deluge.decorators import overrides
|
||||
from deluge.ui.console.modes.basemode import InputKeyHandler
|
||||
from deluge.ui.console.utils import curses_util as util
|
||||
from deluge.ui.console.utils import colors
|
||||
from deluge.ui.console.utils.format_utils import delete_alt_backspace, remove_formatting, wrap_string
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseField(InputKeyHandler):
|
||||
|
||||
def __init__(self, parent=None, name=None, selectable=True, **kwargs):
|
||||
self.name = name
|
||||
self.parent = parent
|
||||
self.fmt_keys = {}
|
||||
self.set_fmt_key("font", "ignore", kwargs)
|
||||
self.set_fmt_key("color", "white,black", kwargs)
|
||||
self.set_fmt_key("color_end", "white,black", kwargs)
|
||||
self.set_fmt_key("color_active", "black,white", kwargs)
|
||||
self.set_fmt_key("color_unfocused", "color", kwargs)
|
||||
self.set_fmt_key("color_unfocused_active", "black,whitegrey", kwargs)
|
||||
self.set_fmt_key("font_active", "font", kwargs)
|
||||
self.set_fmt_key("font_unfocused", "font", kwargs)
|
||||
self.set_fmt_key("font_unfocused_active", "font_active", kwargs)
|
||||
self.default_col = kwargs.get("col", -1)
|
||||
self._selectable = selectable
|
||||
self.value = None
|
||||
|
||||
def selectable(self):
|
||||
return self.has_input() and not self.depend_skip() and self._selectable
|
||||
|
||||
def set_fmt_key(self, key, default, kwargsdict=None):
|
||||
value = self.fmt_keys.get(default, default)
|
||||
if kwargsdict:
|
||||
value = kwargsdict.get(key, value)
|
||||
self.fmt_keys[key] = value
|
||||
|
||||
def get_fmt_keys(self, focused, active, **kwargs):
|
||||
color_key = kwargs.get("color_key", "color")
|
||||
font_key = "font"
|
||||
if not focused:
|
||||
color_key += "_unfocused"
|
||||
font_key += "_unfocused"
|
||||
if active:
|
||||
color_key += "_active"
|
||||
font_key += "_active"
|
||||
return color_key, font_key
|
||||
|
||||
def build_fmt_string(self, focused, active, value_key="msg", **kwargs):
|
||||
color_key, font_key = self.get_fmt_keys(focused, active, **kwargs)
|
||||
return "{!%%(%s)s,%%(%s)s!}%%(%s)s{!%%(%s)s!}" % (color_key, font_key, value_key, "color_end")
|
||||
|
||||
def depend_skip(self):
|
||||
return False
|
||||
|
||||
def has_input(self):
|
||||
return True
|
||||
|
||||
@overrides(InputKeyHandler)
|
||||
def handle_read(self, c):
|
||||
return util.ReadState.IGNORED
|
||||
|
||||
def render(self, screen, row, **kwargs):
|
||||
return 0
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return 1
|
||||
|
||||
def set_value(self, value):
|
||||
self.value = value
|
||||
|
||||
def get_value(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class NoInputField(BaseField):
|
||||
|
||||
@overrides(BaseField)
|
||||
def has_input(self):
|
||||
return False
|
||||
|
||||
|
||||
class InputField(BaseField):
|
||||
|
||||
def __init__(self, parent, name, message, format_default=None, **kwargs):
|
||||
BaseField.__init__(self, parent=parent, name=name, **kwargs)
|
||||
self.format_default = format_default
|
||||
self.message = None
|
||||
self.set_message(message)
|
||||
|
||||
depend = None
|
||||
|
||||
@overrides(BaseField)
|
||||
def handle_read(self, c):
|
||||
if c in [curses.KEY_ENTER, util.KEY_ENTER2, util.KEY_BACKSPACE2, 113]:
|
||||
return util.ReadState.READ
|
||||
return util.ReadState.IGNORED
|
||||
|
||||
def set_message(self, msg):
|
||||
changed = self.message != msg
|
||||
self.message = msg
|
||||
return changed
|
||||
|
||||
def set_depend(self, i, inverse=False):
|
||||
if not isinstance(i, CheckedInput):
|
||||
raise Exception("Can only depend on CheckedInputs")
|
||||
self.depend = i
|
||||
self.inverse = inverse
|
||||
|
||||
def depend_skip(self):
|
||||
if not self.depend:
|
||||
return False
|
||||
if self.inverse:
|
||||
return self.depend.checked
|
||||
else:
|
||||
return not self.depend.checked
|
||||
|
||||
|
||||
class Header(NoInputField):
|
||||
|
||||
def __init__(self, parent, header, space_above, space_below, **kwargs):
|
||||
if "name" not in kwargs:
|
||||
kwargs["name"] = header
|
||||
NoInputField.__init__(self, parent=parent, **kwargs)
|
||||
self.header = "{!white,black,bold!}%s" % header
|
||||
self.space_above = space_above
|
||||
self.space_below = space_below
|
||||
|
||||
@overrides(BaseField)
|
||||
def render(self, screen, row, col=0, **kwargs):
|
||||
rows = 1
|
||||
if self.space_above:
|
||||
row += 1
|
||||
rows += 1
|
||||
self.parent.add_string(row, self.header, scr=screen, col=col, pad=False)
|
||||
if self.space_below:
|
||||
rows += 1
|
||||
return rows
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return 1 + int(self.space_above) + int(self.space_below)
|
||||
|
||||
|
||||
class InfoField(NoInputField):
|
||||
|
||||
def __init__(self, parent, name, label, value, **kwargs):
|
||||
NoInputField.__init__(self, parent=parent, name=name, **kwargs)
|
||||
self.label = label
|
||||
self.value = value
|
||||
self.txt = "%s %s" % (label, value)
|
||||
|
||||
@overrides(BaseField)
|
||||
def render(self, screen, row, col=0, **kwargs):
|
||||
self.parent.add_string(row, self.txt, scr=screen, col=col, pad=False)
|
||||
return 1
|
||||
|
||||
@overrides(BaseField)
|
||||
def set_value(self, v):
|
||||
self.value = v
|
||||
if isinstance(v, float):
|
||||
self.txt = "%s %.2f" % (self.label, self.value)
|
||||
else:
|
||||
self.txt = "%s %s" % (self.label, self.value)
|
||||
|
||||
|
||||
class CheckedInput(InputField):
|
||||
|
||||
def __init__(self, parent, name, message, checked=False, checked_char="X", unchecked_char=" ",
|
||||
checkbox_format="[%s] ", **kwargs):
|
||||
InputField.__init__(self, parent, name, message, **kwargs)
|
||||
self.set_value(checked)
|
||||
self.fmt_keys.update({"msg": message, "checkbox_format": checkbox_format,
|
||||
"unchecked_char": unchecked_char, "checked_char": checked_char})
|
||||
self.set_fmt_key("font_checked", "font", kwargs)
|
||||
self.set_fmt_key("font_unfocused_checked", "font_checked", kwargs)
|
||||
self.set_fmt_key("font_active_checked", "font_active", kwargs)
|
||||
self.set_fmt_key("font_unfocused_active_checked", "font_active_checked", kwargs)
|
||||
self.set_fmt_key("color_checked", "color", kwargs)
|
||||
self.set_fmt_key("color_active_checked", "color_active", kwargs)
|
||||
self.set_fmt_key("color_unfocused_checked", "color_checked", kwargs)
|
||||
self.set_fmt_key("color_unfocused_active_checked", "color_unfocused_active", kwargs)
|
||||
|
||||
@property
|
||||
def checked(self):
|
||||
return self.value
|
||||
|
||||
@overrides(BaseField)
|
||||
def get_fmt_keys(self, focused, active, **kwargs):
|
||||
color_key, font_key = super(CheckedInput, self).get_fmt_keys(focused, active, **kwargs)
|
||||
if self.checked:
|
||||
color_key += "_checked"
|
||||
font_key += "_checked"
|
||||
return color_key, font_key
|
||||
|
||||
def build_msg_string(self, focused, active):
|
||||
fmt_str = self.build_fmt_string(focused, active)
|
||||
char = self.fmt_keys["checked_char" if self.checked else "unchecked_char"]
|
||||
chk_box = ""
|
||||
try:
|
||||
chk_box = self.fmt_keys["checkbox_format"] % char
|
||||
except KeyError:
|
||||
pass
|
||||
msg = fmt_str % self.fmt_keys
|
||||
return chk_box + msg
|
||||
|
||||
@overrides(InputField)
|
||||
def render(self, screen, row, col=0, **kwargs):
|
||||
string = self.build_msg_string(kwargs.get("focused"), kwargs.get("active"))
|
||||
|
||||
self.parent.add_string(row, string, scr=screen, col=col, pad=False)
|
||||
return 1
|
||||
|
||||
@overrides(InputField)
|
||||
def handle_read(self, c):
|
||||
if c == util.KEY_SPACE:
|
||||
self.set_value(not self.checked)
|
||||
return util.ReadState.CHANGED
|
||||
return util.ReadState.IGNORED
|
||||
|
||||
@overrides(InputField)
|
||||
def set_message(self, msg):
|
||||
changed = InputField.set_message(self, msg)
|
||||
if "msg" in self.fmt_keys and self.fmt_keys["msg"] != msg:
|
||||
changed = True
|
||||
self.fmt_keys.update({"msg": msg})
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
class CheckedPlusInput(CheckedInput):
|
||||
|
||||
def __init__(self, parent, name, message, child, child_always_visible=False,
|
||||
show_usage_hints=True, msg_fmt="%s ", **kwargs):
|
||||
CheckedInput.__init__(self, parent, name, message, **kwargs)
|
||||
self.child = child
|
||||
self.child_active = False
|
||||
self.show_usage_hints = show_usage_hints
|
||||
self.msg_fmt = msg_fmt
|
||||
self.child_always_visible = child_always_visible
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return max(2 if self.show_usage_hints else 1, self.child.height)
|
||||
|
||||
@overrides(CheckedInput)
|
||||
def render(self, screen, row, width=None, active=False, focused=False, col=0, **kwargs):
|
||||
isact = active and not self.child_active
|
||||
CheckedInput.render(self, screen, row, width=width, active=isact, focused=focused, col=col)
|
||||
rows = 1
|
||||
if self.show_usage_hints and (self.child_always_visible or (active and self.checked)):
|
||||
msg = "(esc to leave)" if self.child_active else "(right arrow to edit)"
|
||||
self.parent.add_string(row + 1, msg, scr=screen, col=col, pad=False)
|
||||
rows += 1
|
||||
|
||||
msglen = len(self.msg_fmt % colors.strip_colors(self.build_msg_string(focused, active)))
|
||||
# show child
|
||||
if self.checked or self.child_always_visible:
|
||||
crows = self.child.render(screen, row, width=width - msglen,
|
||||
active=self.child_active and active,
|
||||
col=col + msglen, cursor_offset=msglen)
|
||||
rows = max(rows, crows)
|
||||
else:
|
||||
self.parent.add_string(row, "(enable to view/edit value)", scr=screen,
|
||||
col=col + msglen, pad=False)
|
||||
return rows
|
||||
|
||||
@overrides(CheckedInput)
|
||||
def handle_read(self, c):
|
||||
if self.child_active:
|
||||
if c == util.KEY_ESC: # leave child on esc
|
||||
self.child_active = False
|
||||
return util.ReadState.READ
|
||||
# pass keys through to child
|
||||
return self.child.handle_read(c)
|
||||
else:
|
||||
if c == util.KEY_SPACE:
|
||||
self.set_value(not self.checked)
|
||||
return util.ReadState.CHANGED
|
||||
if (self.checked or self.child_always_visible) and c == curses.KEY_RIGHT:
|
||||
self.child_active = True
|
||||
return util.ReadState.READ
|
||||
return util.ReadState.IGNORED
|
||||
|
||||
def get_child(self):
|
||||
return self.child
|
||||
|
||||
|
||||
class IntSpinInput(InputField):
|
||||
|
||||
def __init__(self, parent, name, message, move_func, value, min_val=None, max_val=None,
|
||||
inc_amt=1, incr_large=10, strict_validation=False, fmt="%d", **kwargs):
|
||||
InputField.__init__(self, parent, name, message, **kwargs)
|
||||
self.convert_func = int
|
||||
self.fmt = fmt
|
||||
self.valstr = str(value)
|
||||
self.default_str = self.valstr
|
||||
self.set_value(value)
|
||||
self.default_value = self.value
|
||||
self.last_valid_value = self.value
|
||||
self.last_active = False
|
||||
self.cursor = len(self.valstr)
|
||||
self.cursoff = colors.get_line_width(self.message) + 3 # + 4 for the " [ " in the rendered string
|
||||
self.move_func = move_func
|
||||
self.strict_validation = strict_validation
|
||||
self.min_val = min_val
|
||||
self.max_val = max_val
|
||||
self.inc_amt = inc_amt
|
||||
self.incr_large = incr_large
|
||||
|
||||
def validate_value(self, value, on_invalid=None):
|
||||
if (self.min_val is not None) and value < self.min_val:
|
||||
value = on_invalid if on_invalid else self.min_val
|
||||
if (self.max_val is not None) and value > self.max_val:
|
||||
value = on_invalid if on_invalid else self.max_val
|
||||
return value
|
||||
|
||||
@overrides(InputField)
|
||||
def render(self, screen, row, active=False, focused=True, col=0, cursor_offset=0, **kwargs):
|
||||
if active:
|
||||
self.last_active = True
|
||||
elif self.last_active:
|
||||
self.set_value(self.valstr, validate=True, value_on_fail=self.last_valid_value)
|
||||
self.last_active = False
|
||||
|
||||
fmt_str = self.build_fmt_string(focused, active, value_key="value")
|
||||
value_format = "%(msg)s {!input!}"
|
||||
if not self.valstr:
|
||||
value_format += "[ ]"
|
||||
elif self.format_default and self.valstr == self.default_str:
|
||||
value_format += "[ {!magenta,black!}%(value)s{!input!} ]"
|
||||
else:
|
||||
value_format += "[ " + fmt_str + " ]"
|
||||
|
||||
self.parent.add_string(row, value_format % dict({"msg": self.message, "value": "%s" % self.valstr},
|
||||
**self.fmt_keys),
|
||||
scr=screen, col=col, pad=False)
|
||||
if active:
|
||||
if focused:
|
||||
util.safe_curs_set(util.Curser.NORMAL)
|
||||
self.move_func(row, self.cursor + self.cursoff + cursor_offset)
|
||||
else:
|
||||
util.safe_curs_set(util.Curser.INVISIBLE)
|
||||
return 1
|
||||
|
||||
@overrides(InputField)
|
||||
def handle_read(self, c):
|
||||
if c == util.KEY_SPACE:
|
||||
return util.ReadState.READ
|
||||
elif c == curses.KEY_PPAGE:
|
||||
self.set_value(self.value + self.inc_amt, validate=True)
|
||||
elif c == curses.KEY_NPAGE:
|
||||
self.set_value(self.value - self.inc_amt, validate=True)
|
||||
elif c == util.KEY_ALT_AND_KEY_PPAGE:
|
||||
self.set_value(self.value + self.incr_large, validate=True)
|
||||
elif c == util.KEY_ALT_AND_KEY_NPAGE:
|
||||
self.set_value(self.value - self.incr_large, validate=True)
|
||||
elif c == curses.KEY_LEFT:
|
||||
self.cursor = max(0, self.cursor - 1)
|
||||
elif c == curses.KEY_RIGHT:
|
||||
self.cursor = min(len(self.valstr), self.cursor + 1)
|
||||
elif c == curses.KEY_HOME:
|
||||
self.cursor = 0
|
||||
elif c == curses.KEY_END:
|
||||
self.cursor = len(self.valstr)
|
||||
elif c == curses.KEY_BACKSPACE or c == util.KEY_BACKSPACE2:
|
||||
if self.valstr and self.cursor > 0:
|
||||
new_val = self.valstr[:self.cursor - 1] + self.valstr[self.cursor:]
|
||||
self.set_value(new_val, validate=False, cursor=self.cursor - 1, cursor_on_fail=True,
|
||||
value_on_fail=self.valstr if self.strict_validation else None)
|
||||
elif c == curses.KEY_DC: # Del
|
||||
if self.valstr and self.cursor <= len(self.valstr):
|
||||
if self.cursor == 0:
|
||||
new_val = self.valstr[1:]
|
||||
else:
|
||||
new_val = self.valstr[:self.cursor] + self.valstr[self.cursor + 1:]
|
||||
self.set_value(new_val, validate=False, cursor=False,
|
||||
value_on_fail=self.valstr if self.strict_validation else None, cursor_on_fail=True)
|
||||
elif c == ord('-'): # minus
|
||||
self.set_value(self.value - 1, validate=True, cursor=True, cursor_on_fail=True,
|
||||
value_on_fail=self.value, on_invalid=self.value)
|
||||
elif c == ord('+'): # plus
|
||||
self.set_value(self.value + 1, validate=True, cursor=True, cursor_on_fail=True,
|
||||
value_on_fail=self.value, on_invalid=self.value)
|
||||
elif util.is_int_chr(c):
|
||||
if self.strict_validation:
|
||||
new_val = self.valstr[:self.cursor - 1] + chr(c) + self.valstr[self.cursor - 1:]
|
||||
self.set_value(new_val, validate=True, cursor=self.cursor + 1,
|
||||
value_on_fail=self.valstr, on_invalid=self.value)
|
||||
else:
|
||||
minus_place = self.valstr.find("-")
|
||||
if self.cursor > minus_place:
|
||||
new_val = self.valstr[:self.cursor] + chr(c) + self.valstr[self.cursor:]
|
||||
self.set_value(new_val, validate=True, cursor=self.cursor + 1, on_invalid=self.value)
|
||||
else:
|
||||
return util.ReadState.IGNORED
|
||||
return util.ReadState.READ
|
||||
|
||||
@overrides(BaseField)
|
||||
def set_value(self, val, cursor=True, validate=False, cursor_on_fail=False, value_on_fail=None, on_invalid=None):
|
||||
value = None
|
||||
try:
|
||||
value = self.convert_func(val)
|
||||
if validate:
|
||||
validated = self.validate_value(value, on_invalid)
|
||||
if validated != value:
|
||||
# Value was not valid, so use validated value instead.
|
||||
# Also set cursor according to validated value
|
||||
cursor = True
|
||||
value = validated
|
||||
|
||||
new_valstr = self.fmt % value
|
||||
if new_valstr == self.valstr:
|
||||
# If string has not change, keep cursor
|
||||
cursor = False
|
||||
self.valstr = new_valstr
|
||||
self.last_valid_value = self.value = value
|
||||
except ValueError:
|
||||
if value_on_fail is not None:
|
||||
self.set_value(value_on_fail, cursor=cursor, cursor_on_fail=cursor_on_fail,
|
||||
validate=validate, on_invalid=on_invalid)
|
||||
return
|
||||
self.value = None
|
||||
self.valstr = val
|
||||
if cursor_on_fail:
|
||||
self.cursor = cursor
|
||||
except TypeError:
|
||||
import traceback
|
||||
log.warn("TypeError: %s", "".join(traceback.format_exc()))
|
||||
else:
|
||||
if cursor is True:
|
||||
self.cursor = len(self.valstr)
|
||||
elif cursor is not False:
|
||||
self.cursor = cursor
|
||||
|
||||
|
||||
class FloatSpinInput(IntSpinInput):
|
||||
|
||||
def __init__(self, parent, message, name, move_func, value, precision=1, **kwargs):
|
||||
self.precision = precision
|
||||
IntSpinInput.__init__(self, parent, message, name, move_func, value, **kwargs)
|
||||
self.fmt = "%%.%df" % precision
|
||||
self.convert_func = lambda valstr: round(float(valstr), self.precision)
|
||||
self.set_value(value)
|
||||
self.cursor = len(self.valstr)
|
||||
|
||||
@overrides(IntSpinInput)
|
||||
def handle_read(self, c):
|
||||
if c == ord('.'):
|
||||
minus_place = self.valstr.find("-")
|
||||
if self.cursor <= minus_place:
|
||||
return util.ReadState.READ
|
||||
point_place = self.valstr.find(".")
|
||||
if point_place >= 0:
|
||||
return util.ReadState.READ
|
||||
new_val = self.valstr[:self.cursor] + chr(c) + self.valstr[self.cursor:]
|
||||
self.set_value(new_val, validate=True, cursor=self.cursor + 1)
|
||||
else:
|
||||
return IntSpinInput.handle_read(self, c)
|
||||
|
||||
|
||||
class SelectInput(InputField):
|
||||
|
||||
def __init__(self, parent, name, message, opts, vals, active_index, active_default=False,
|
||||
require_select_action=True, **kwargs):
|
||||
InputField.__init__(self, parent, name, message, **kwargs)
|
||||
self.opts = opts
|
||||
self.vals = vals
|
||||
self.active_index = active_index
|
||||
self.selected_index = active_index
|
||||
self.default_option = active_index if active_default else None
|
||||
self.require_select_action = require_select_action
|
||||
self.fmt_keys.update({"font_active": "bold"})
|
||||
font_selected = kwargs.get("font_selected", "bold,underline")
|
||||
|
||||
self.set_fmt_key("font_selected", font_selected, kwargs)
|
||||
self.set_fmt_key("font_active_selected", "font_selected", kwargs)
|
||||
self.set_fmt_key("font_unfocused_selected", "font_selected", kwargs)
|
||||
self.set_fmt_key("font_unfocused_active_selected", "font_active_selected", kwargs)
|
||||
|
||||
self.set_fmt_key("color_selected", "color", kwargs)
|
||||
self.set_fmt_key("color_active_selected", "color_active", kwargs)
|
||||
self.set_fmt_key("color_unfocused_selected", "color_selected", kwargs)
|
||||
self.set_fmt_key("color_unfocused_active_selected", "color_unfocused_active", kwargs)
|
||||
self.set_fmt_key("color_default_value", "magenta,black", kwargs)
|
||||
|
||||
self.set_fmt_key("color_default_value", "magenta,black")
|
||||
self.set_fmt_key("color_default_value_active", "magentadark,white")
|
||||
self.set_fmt_key("color_default_value_selected", "color_default_value", kwargs)
|
||||
self.set_fmt_key("color_default_value_unfocused", "color_default_value", kwargs)
|
||||
self.set_fmt_key("color_default_value_unfocused_selected", "color_default_value_selected", kwargs)
|
||||
self.set_fmt_key("color_default_value_active_selected", "magentadark,white")
|
||||
self.set_fmt_key("color_default_value_unfocused_active_selected", "color_unfocused_active", kwargs)
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return 1 + bool(self.message)
|
||||
|
||||
@overrides(BaseField)
|
||||
def get_fmt_keys(self, focused, active, selected=False, **kwargs):
|
||||
color_key, font_key = super(SelectInput, self).get_fmt_keys(focused, active, **kwargs)
|
||||
if selected:
|
||||
color_key += "_selected"
|
||||
font_key += "_selected"
|
||||
return color_key, font_key
|
||||
|
||||
@overrides(InputField)
|
||||
def render(self, screen, row, active=False, focused=True, col=0, **kwargs):
|
||||
if self.message:
|
||||
self.parent.add_string(row, self.message, scr=screen, col=col, pad=False)
|
||||
row += 1
|
||||
|
||||
off = col + 1
|
||||
for i, opt in enumerate(self.opts):
|
||||
self.fmt_keys["msg"] = opt
|
||||
fmt_args = {"selected": i == self.selected_index}
|
||||
if i == self.default_option:
|
||||
fmt_args["color_key"] = "color_default_value"
|
||||
fmt = self.build_fmt_string(focused, (i == self.active_index) and active, **fmt_args)
|
||||
string = "[%s]" % (fmt % self.fmt_keys)
|
||||
self.parent.add_string(row, string, scr=screen, col=off, pad=False)
|
||||
off += len(opt) + 3
|
||||
if self.message:
|
||||
return 2
|
||||
else:
|
||||
return 1
|
||||
|
||||
@overrides(InputField)
|
||||
def handle_read(self, c):
|
||||
if c == curses.KEY_LEFT:
|
||||
self.active_index = max(0, self.active_index - 1)
|
||||
if not self.require_select_action:
|
||||
self.selected_index = self.active_index
|
||||
elif c == curses.KEY_RIGHT:
|
||||
self.active_index = min(len(self.opts) - 1, self.active_index + 1)
|
||||
if not self.require_select_action:
|
||||
self.selected_index = self.active_index
|
||||
elif c == ord(' '):
|
||||
if self.require_select_action:
|
||||
self.selected_index = self.active_index
|
||||
else:
|
||||
return util.ReadState.IGNORED
|
||||
return util.ReadState.READ
|
||||
|
||||
@overrides(BaseField)
|
||||
def get_value(self):
|
||||
return self.vals[self.selected_index]
|
||||
|
||||
@overrides(BaseField)
|
||||
def set_value(self, value):
|
||||
for i, val in enumerate(self.vals):
|
||||
if value == val:
|
||||
self.selected_index = i
|
||||
return
|
||||
raise Exception("Invalid value for SelectInput")
|
||||
|
||||
|
||||
class TextInput(InputField):
|
||||
|
||||
def __init__(self, parent, name, message, move_func, width, value, complete=False,
|
||||
activate_input=False, **kwargs):
|
||||
InputField.__init__(self, parent, name, message, **kwargs)
|
||||
self.move_func = move_func
|
||||
self._width = width
|
||||
self.value = value
|
||||
self.default_value = value
|
||||
self.complete = complete
|
||||
self.tab_count = 0
|
||||
self.cursor = len(self.value)
|
||||
self.opts = None
|
||||
self.opt_off = 0
|
||||
self.value_offset = 0
|
||||
self.activate_input = activate_input # Wether input must be activated
|
||||
self.input_active = not self.activate_input
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self._width
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return 1 + bool(self.message)
|
||||
|
||||
def calculate_textfield_value(self, width, cursor_offset):
|
||||
cursor_width = width
|
||||
|
||||
if self.cursor > (cursor_width - 1):
|
||||
c_pos_abs = self.cursor - cursor_width
|
||||
if cursor_width <= (self.cursor - self.value_offset):
|
||||
new_cur = c_pos_abs + 1
|
||||
self.value_offset = new_cur
|
||||
else:
|
||||
if self.cursor >= len(self.value):
|
||||
c_pos_abs = len(self.value) - cursor_width
|
||||
new_cur = c_pos_abs + 1
|
||||
self.value_offset = new_cur
|
||||
vstr = self.value[self.value_offset:]
|
||||
|
||||
if len(vstr) > cursor_width:
|
||||
vstr = vstr[:cursor_width]
|
||||
vstr = vstr.ljust(cursor_width)
|
||||
else:
|
||||
if len(self.value) <= cursor_width:
|
||||
self.value_offset = 0
|
||||
vstr = self.value.ljust(cursor_width)
|
||||
else:
|
||||
self.value_offset = min(self.value_offset, self.cursor)
|
||||
vstr = self.value[self.value_offset:]
|
||||
if len(vstr) > cursor_width:
|
||||
vstr = vstr[:cursor_width]
|
||||
vstr = vstr.ljust(cursor_width)
|
||||
|
||||
return vstr
|
||||
|
||||
def calculate_cursor_pos(self, width, col):
|
||||
cursor_width = width
|
||||
x_pos = self.cursor + col
|
||||
|
||||
if (self.cursor + col - self.value_offset) > cursor_width:
|
||||
x_pos += self.value_offset
|
||||
else:
|
||||
x_pos -= self.value_offset
|
||||
|
||||
return min(width - 1 + col, x_pos)
|
||||
|
||||
@overrides(InputField)
|
||||
def render(self, screen, row, width=None, active=False, focused=True, col=0, cursor_offset=0, **kwargs):
|
||||
if not self.value and not active and len(self.default_value) != 0:
|
||||
self.value = self.default_value
|
||||
self.cursor = len(self.value)
|
||||
|
||||
if self.message:
|
||||
self.parent.add_string(row, self.message, scr=screen, col=col, pad=False)
|
||||
row += 1
|
||||
|
||||
vstr = self.calculate_textfield_value(width, cursor_offset)
|
||||
|
||||
if active:
|
||||
if self.opts:
|
||||
self.parent.add_string(row + 1, self.opts[self.opt_off:], scr=screen, col=col, pad=False)
|
||||
|
||||
if focused and self.input_active:
|
||||
util.safe_curs_set(util.Curser.NORMAL) # Make cursor visible when text field is focused
|
||||
x_pos = self.calculate_cursor_pos(width, col)
|
||||
self.move_func(row, x_pos)
|
||||
|
||||
fmt = "{!black,white,bold!}%s"
|
||||
if self.format_default and len(self.value) != 0 and self.value == self.default_value:
|
||||
fmt = "{!magenta,white!}%s"
|
||||
if not active or not focused or self.input_active:
|
||||
fmt = "{!white,grey,bold!}%s"
|
||||
|
||||
self.parent.add_string(row, fmt % vstr, scr=screen, col=col, pad=False, trim=False)
|
||||
return self.height
|
||||
|
||||
@overrides(BaseField)
|
||||
def set_value(self, val):
|
||||
self.value = val
|
||||
self.cursor = len(self.value)
|
||||
|
||||
@overrides(InputField)
|
||||
def handle_read(self, c):
|
||||
"""
|
||||
Return False when key was swallowed, i.e. we recognised
|
||||
the key and no further action by other components should
|
||||
be performed.
|
||||
"""
|
||||
if self.activate_input:
|
||||
if not self.input_active:
|
||||
if c in [curses.KEY_LEFT, curses.KEY_RIGHT, curses.KEY_HOME,
|
||||
curses.KEY_END, curses.KEY_ENTER, util.KEY_ENTER2]:
|
||||
self.input_active = True
|
||||
return util.ReadState.READ
|
||||
else:
|
||||
return util.ReadState.IGNORED
|
||||
elif c == util.KEY_ESC:
|
||||
self.input_active = False
|
||||
return util.ReadState.READ
|
||||
|
||||
if c == util.KEY_TAB and self.complete:
|
||||
# Keep track of tab hit count to know when it's double-hit
|
||||
self.tab_count += 1
|
||||
if self.tab_count > 1:
|
||||
second_hit = True
|
||||
self.tab_count = 0
|
||||
else:
|
||||
second_hit = False
|
||||
|
||||
# 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.cursor == len(self.value) or self.value[self.cursor] == " ":
|
||||
if self.opts:
|
||||
prev = self.opt_off
|
||||
self.opt_off += self.width - 3
|
||||
# now find previous double space, best guess at a split point
|
||||
# in future could keep opts unjoined to get this really right
|
||||
self.opt_off = self.opts.rfind(" ", 0, self.opt_off) + 2
|
||||
if second_hit and self.opt_off == prev: # double tap and we're at the end
|
||||
self.opt_off = 0
|
||||
else:
|
||||
opts = self.do_complete(self.value)
|
||||
if len(opts) == 1: # only one option, just complete it
|
||||
self.value = opts[0]
|
||||
self.cursor = len(opts[0])
|
||||
self.tab_count = 0
|
||||
elif len(opts) > 1:
|
||||
prefix = os.path.commonprefix(opts)
|
||||
if prefix:
|
||||
self.value = prefix
|
||||
self.cursor = len(prefix)
|
||||
|
||||
if len(opts) > 1 and second_hit: # display multiple options on second tab hit
|
||||
sp = self.value.rfind(os.sep) + 1
|
||||
self.opts = " ".join([o[sp:] for o in opts])
|
||||
|
||||
# Cursor movement
|
||||
elif c == curses.KEY_LEFT:
|
||||
self.cursor = max(0, self.cursor - 1)
|
||||
elif c == curses.KEY_RIGHT:
|
||||
self.cursor = min(len(self.value), self.cursor + 1)
|
||||
elif c == curses.KEY_HOME:
|
||||
self.cursor = 0
|
||||
elif c == curses.KEY_END:
|
||||
self.cursor = len(self.value)
|
||||
|
||||
# Delete a character in the input string based on cursor position
|
||||
elif c == curses.KEY_BACKSPACE or c == util.KEY_BACKSPACE2:
|
||||
if self.value and self.cursor > 0:
|
||||
self.value = self.value[:self.cursor - 1] + self.value[self.cursor:]
|
||||
self.cursor -= 1
|
||||
elif c == [util.KEY_ESC, util.KEY_BACKSPACE2] or c == [util.KEY_ESC, curses.KEY_BACKSPACE]:
|
||||
self.value, self.cursor = delete_alt_backspace(self.value, self.cursor)
|
||||
elif c == curses.KEY_DC:
|
||||
if self.value and self.cursor < len(self.value):
|
||||
self.value = self.value[:self.cursor] + self.value[self.cursor + 1:]
|
||||
elif c > 31 and c < 256:
|
||||
# Emulate getwch
|
||||
stroke = chr(c)
|
||||
uchar = ""
|
||||
while not uchar:
|
||||
try:
|
||||
uchar = stroke.decode(self.parent.encoding)
|
||||
except UnicodeDecodeError:
|
||||
c = self.parent.parent.stdscr.getch()
|
||||
stroke += chr(c)
|
||||
if uchar:
|
||||
if self.cursor == len(self.value):
|
||||
self.value += uchar
|
||||
else:
|
||||
# Insert into string
|
||||
self.value = self.value[:self.cursor] + uchar + self.value[self.cursor:]
|
||||
# Move the cursor forward
|
||||
self.cursor += 1
|
||||
|
||||
else:
|
||||
self.opts = None
|
||||
self.opt_off = 0
|
||||
self.tab_count = 0
|
||||
return util.ReadState.IGNORED
|
||||
return util.ReadState.READ
|
||||
|
||||
def do_complete(self, line):
|
||||
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
|
||||
for f in os.listdir(line):
|
||||
# Skip hidden
|
||||
if f.startswith("."):
|
||||
continue
|
||||
f = os.path.join(line, f)
|
||||
if os.path.isdir(f):
|
||||
f += os.sep
|
||||
ret.append(f)
|
||||
else:
|
||||
# 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))
|
||||
else:
|
||||
# This path does not exist, so lets do a listdir on it's parent
|
||||
# and find any matches.
|
||||
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):
|
||||
p += os.sep
|
||||
ret.append(p)
|
||||
return ret
|
||||
|
||||
|
||||
class ComboInput(InputField):
|
||||
|
||||
def __init__(self, parent, name, message, choices, default=None, searchable=True, **kwargs):
|
||||
InputField.__init__(self, parent, name, message, **kwargs)
|
||||
self.choices = choices
|
||||
self.default = default
|
||||
self.set_value(default)
|
||||
max_width = 0
|
||||
for c in choices:
|
||||
max_width = max(max_width, len(c[1]))
|
||||
self.choices_width = max_width
|
||||
self.searchable = searchable
|
||||
|
||||
@overrides(BaseField)
|
||||
def render(self, screen, row, col=0, **kwargs):
|
||||
fmt_str = self.build_fmt_string(kwargs.get("focused"), kwargs.get("active"))
|
||||
string = "%s: [%10s]" % (self.message, fmt_str % self.fmt_keys)
|
||||
self.parent.add_string(row, string, scr=screen, col=col, pad=False)
|
||||
return 1
|
||||
|
||||
def _lang_selected(self, selected, *args, **kwargs):
|
||||
if selected is not None:
|
||||
self.set_value(selected)
|
||||
self.parent.pop_popup()
|
||||
|
||||
@overrides(InputField)
|
||||
def handle_read(self, c):
|
||||
if c in [util.KEY_SPACE, curses.KEY_ENTER, util.KEY_ENTER2]:
|
||||
|
||||
def search_handler(key):
|
||||
"""Handle keyboard input to seach the list"""
|
||||
if not util.is_printable_char(key):
|
||||
return
|
||||
selected = select_popup.current_selection()
|
||||
|
||||
def select_in_range(begin, end):
|
||||
for i in range(begin, end):
|
||||
val = select_popup.inputs[i].get_value()
|
||||
if val.lower().startswith(chr(key)):
|
||||
select_popup.set_selection(i)
|
||||
return True
|
||||
return False
|
||||
|
||||
# First search downwards
|
||||
if not select_in_range(selected + 1, len(select_popup.inputs)):
|
||||
# No match, so start at beginning
|
||||
select_in_range(0, selected)
|
||||
|
||||
from deluge.ui.console.widgets.popup import SelectablePopup # Must import here
|
||||
select_popup = SelectablePopup(self.parent, " %s " % _("Select Language"), self._lang_selected,
|
||||
input_cb=search_handler if self.searchable else None,
|
||||
border_off_west=1, active_wrap=False, width_req=self.choices_width + 12)
|
||||
for choice in self.choices:
|
||||
args = {"data": choice[0]}
|
||||
select_popup.add_line(choice[0], choice[1], selectable=True,
|
||||
selected=choice[0] == self.get_value(), **args)
|
||||
self.parent.push_popup(select_popup)
|
||||
return util.ReadState.CHANGED
|
||||
return util.ReadState.IGNORED
|
||||
|
||||
@overrides(BaseField)
|
||||
def set_value(self, val):
|
||||
self.value = val
|
||||
msg = None
|
||||
for c in self.choices:
|
||||
if c[0] == val:
|
||||
msg = c[1]
|
||||
break
|
||||
if msg is None:
|
||||
log.warn("Setting a value '%s' found found in choices: %s", val, self.choices)
|
||||
self.fmt_keys.update({"msg": msg})
|
||||
|
||||
|
||||
class TextField(BaseField):
|
||||
|
||||
def __init__(self, parent, name, value, selectable=True, value_fmt="%s", **kwargs):
|
||||
BaseField.__init__(self, parent=parent, name=name, selectable=selectable, **kwargs)
|
||||
self.value = value
|
||||
self.value_fmt = value_fmt
|
||||
self.set_value(value)
|
||||
|
||||
@overrides(BaseField)
|
||||
def set_value(self, value):
|
||||
self.value = value
|
||||
self.txt = self.value_fmt % (value)
|
||||
|
||||
@overrides(BaseField)
|
||||
def has_input(self):
|
||||
return True
|
||||
|
||||
@overrides(BaseField)
|
||||
def render(self, screen, row, active=False, focused=False, col=0, **kwargs):
|
||||
util.safe_curs_set(util.Curser.INVISIBLE) # Make cursor invisible when text field is active
|
||||
fmt = self.build_fmt_string(focused, active)
|
||||
self.fmt_keys["msg"] = self.txt
|
||||
string = fmt % self.fmt_keys
|
||||
self.parent.add_string(row, string, scr=screen, col=col, pad=False, trim=False)
|
||||
return 1
|
||||
|
||||
|
||||
class TextArea(TextField):
|
||||
|
||||
def __init__(self, parent, name, value, value_fmt="%s", **kwargs):
|
||||
TextField.__init__(self, parent, name, value, selectable=False, value_fmt=value_fmt, **kwargs)
|
||||
|
||||
@overrides(TextField)
|
||||
def render(self, screen, row, col=0, **kwargs):
|
||||
util.safe_curs_set(util.Curser.INVISIBLE) # Make cursor invisible when text field is active
|
||||
color = "{!white,black!}"
|
||||
lines = wrap_string(self.txt, self.parent.width - 3, 3, True)
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
self.parent.add_string(row + i, "%s%s" % (color, line), scr=screen, col=col, pad=False, trim=False)
|
||||
return len(lines)
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
lines = wrap_string(self.txt, self.parent.width - 3, 3, True)
|
||||
return len(lines)
|
||||
|
||||
@overrides(TextField)
|
||||
def has_input(self):
|
||||
return False
|
||||
|
||||
|
||||
class DividerField(NoInputField):
|
||||
|
||||
def __init__(self, parent, name, value, selectable=False, fill_width=True, value_fmt="%s", **kwargs):
|
||||
NoInputField.__init__(self, parent=parent, name=name, selectable=selectable, **kwargs)
|
||||
self.value = value
|
||||
self.value_fmt = value_fmt
|
||||
self.set_value(value)
|
||||
self.fill_width = fill_width
|
||||
|
||||
@overrides(BaseField)
|
||||
def set_value(self, value):
|
||||
self.value = value
|
||||
self.txt = self.value_fmt % (value)
|
||||
|
||||
@overrides(BaseField)
|
||||
def render(self, screen, row, active=False, focused=False, col=0, width=None, **kwargs):
|
||||
util.safe_curs_set(util.Curser.INVISIBLE) # Make cursor invisible when text field is active
|
||||
fmt = self.build_fmt_string(focused, active)
|
||||
self.fmt_keys["msg"] = self.txt
|
||||
if self.fill_width:
|
||||
self.fmt_keys["msg"] = ""
|
||||
string_len = len(remove_formatting(fmt % self.fmt_keys))
|
||||
fill_len = width - string_len - (len(self.txt) - 1)
|
||||
self.fmt_keys["msg"] = self.txt * fill_len
|
||||
string = fmt % self.fmt_keys
|
||||
self.parent.add_string(row, string, scr=screen, col=col, pad=False, trim=False)
|
||||
return 1
|
310
deluge/ui/console/widgets/inputpane.py
Normal file
310
deluge/ui/console/widgets/inputpane.py
Normal file
@ -0,0 +1,310 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||
# 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.
|
||||
#
|
||||
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import logging
|
||||
|
||||
from deluge.decorators import overrides
|
||||
from deluge.ui.console.modes.basemode import InputKeyHandler, move_cursor
|
||||
from deluge.ui.console.utils import curses_util as util
|
||||
from deluge.ui.console.widgets.fields import (CheckedInput, CheckedPlusInput, ComboInput, DividerField, FloatSpinInput,
|
||||
Header, InfoField, IntSpinInput, NoInputField, SelectInput, TextArea,
|
||||
TextField, TextInput)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseInputPane(InputKeyHandler):
|
||||
|
||||
def __init__(self, mode, allow_rearrange=False, immediate_action=False, set_first_input_active=True,
|
||||
border_off_west=0, border_off_north=0, border_off_east=0, border_off_south=0,
|
||||
active_wrap=False, **kwargs):
|
||||
self.inputs = []
|
||||
self.mode = mode
|
||||
self.active_input = 0
|
||||
self.set_first_input_active = set_first_input_active
|
||||
self.allow_rearrange = allow_rearrange
|
||||
self.immediate_action = immediate_action
|
||||
self.move_active_many = 4
|
||||
self.active_wrap = active_wrap
|
||||
self.lineoff = 0
|
||||
self.border_off_west = border_off_west
|
||||
self.border_off_north = border_off_north
|
||||
self.border_off_east = border_off_east
|
||||
self.border_off_south = border_off_south
|
||||
self.last_lineoff_move = 0
|
||||
|
||||
if not hasattr(self, "visible_content_pane_height"):
|
||||
log.error("The class '%s' does not have the attribute '%s' required by super class '%s'",
|
||||
self.__class__.__name__, "visible_content_pane_height", BaseInputPane.__name__)
|
||||
raise AttributeError("visible_content_pane_height")
|
||||
|
||||
@property
|
||||
def visible_content_pane_width(self):
|
||||
return self.mode.width
|
||||
|
||||
def add_spaces(self, num):
|
||||
string = ""
|
||||
for i in range(num):
|
||||
string += "\n"
|
||||
|
||||
self.add_text_area("space %d" % len(self.inputs), string)
|
||||
|
||||
def add_text(self, string):
|
||||
self.add_text_area("", string)
|
||||
|
||||
def move(self, r, c):
|
||||
self._cursor_row = r
|
||||
self._cursor_col = c
|
||||
|
||||
def get_input(self, name):
|
||||
for e in self.inputs:
|
||||
if e.name == name:
|
||||
return e
|
||||
|
||||
def _add_input(self, input_element):
|
||||
for e in self.inputs:
|
||||
if isinstance(e, NoInputField):
|
||||
continue
|
||||
if e.name == input_element.name:
|
||||
import traceback
|
||||
log.warn("Input element with name '%s' already exists in input pane (%s):\n%s",
|
||||
input_element.name, e, "".join(traceback.format_stack(limit=5)))
|
||||
return
|
||||
|
||||
self.inputs.append(input_element)
|
||||
if self.set_first_input_active and input_element.selectable():
|
||||
self.active_input = len(self.inputs) - 1
|
||||
self.set_first_input_active = False
|
||||
return input_element
|
||||
|
||||
def add_header(self, header, space_above=False, space_below=False, **kwargs):
|
||||
return self._add_input(Header(self, header, space_above, space_below, **kwargs))
|
||||
|
||||
def add_info_field(self, name, label, value):
|
||||
return self._add_input(InfoField(self, name, label, value))
|
||||
|
||||
def add_text_field(self, name, message, selectable=True, col="+1", **kwargs):
|
||||
return self._add_input(TextField(self, name, message, selectable=selectable, col=col, **kwargs))
|
||||
|
||||
def add_text_area(self, name, message, **kwargs):
|
||||
return self._add_input(TextArea(self, name, message, **kwargs))
|
||||
|
||||
def add_divider_field(self, name, message, **kwargs):
|
||||
return self._add_input(DividerField(self, name, message, **kwargs))
|
||||
|
||||
def add_text_input(self, name, message, value="", col="+1", **kwargs):
|
||||
"""
|
||||
Add a text input field
|
||||
|
||||
:param message: string to display above the input field
|
||||
:param name: name of the field, for the return callback
|
||||
:param value: initial value of the field
|
||||
:param complete: should completion be run when tab is hit and this field is active
|
||||
"""
|
||||
return self._add_input(TextInput(self, name, message, self.move, self.visible_content_pane_width, value,
|
||||
col=col, **kwargs))
|
||||
|
||||
def add_select_input(self, name, message, opts, vals, default_index=0, **kwargs):
|
||||
return self._add_input(SelectInput(self, name, message, opts, vals, default_index, **kwargs))
|
||||
|
||||
def add_checked_input(self, name, message, checked=False, col="+1", **kwargs):
|
||||
return self._add_input(CheckedInput(self, name, message, checked=checked, col=col, **kwargs))
|
||||
|
||||
def add_checkedplus_input(self, name, message, child, checked=False, col="+1", **kwargs):
|
||||
return self._add_input(CheckedPlusInput(self, name, message, child, checked=checked, col=col, **kwargs))
|
||||
|
||||
def add_float_spin_input(self, name, message, value=0.0, col="+1", **kwargs):
|
||||
return self._add_input(FloatSpinInput(self, name, message, self.move, value, col=col, **kwargs))
|
||||
|
||||
def add_int_spin_input(self, name, message, value=0, col="+1", **kwargs):
|
||||
return self._add_input(IntSpinInput(self, name, message, self.move, value, col=col, **kwargs))
|
||||
|
||||
def add_combo_input(self, name, message, choices, col="+1", **kwargs):
|
||||
return self._add_input(ComboInput(self, name, message, choices, col=col, **kwargs))
|
||||
|
||||
@overrides(InputKeyHandler)
|
||||
def handle_read(self, c):
|
||||
if not self.inputs: # no inputs added yet
|
||||
return util.ReadState.IGNORED
|
||||
ret = self.inputs[self.active_input].handle_read(c)
|
||||
if ret != util.ReadState.IGNORED:
|
||||
if self.immediate_action:
|
||||
self.immediate_action_cb(state_changed=False if ret == util.ReadState.READ else True)
|
||||
return ret
|
||||
|
||||
ret = util.ReadState.READ
|
||||
|
||||
if c == curses.KEY_UP:
|
||||
self.move_active_up(1)
|
||||
elif c == curses.KEY_DOWN:
|
||||
self.move_active_down(1)
|
||||
elif c == curses.KEY_HOME:
|
||||
self.move_active_up(len(self.inputs))
|
||||
elif c == curses.KEY_END:
|
||||
self.move_active_down(len(self.inputs))
|
||||
elif c == curses.KEY_PPAGE:
|
||||
self.move_active_up(self.move_active_many)
|
||||
elif c == curses.KEY_NPAGE:
|
||||
self.move_active_down(self.move_active_many)
|
||||
elif c == util.KEY_ALT_AND_ARROW_UP:
|
||||
self.lineoff = max(self.lineoff - 1, 0)
|
||||
elif c == util.KEY_ALT_AND_ARROW_DOWN:
|
||||
tot_height = self.get_content_height()
|
||||
self.lineoff = min(self.lineoff + 1, tot_height - self.visible_content_pane_height)
|
||||
elif c == util.KEY_CTRL_AND_ARROW_UP:
|
||||
if not self.allow_rearrange:
|
||||
return ret
|
||||
val = self.inputs.pop(self.active_input)
|
||||
self.active_input -= 1
|
||||
self.inputs.insert(self.active_input, val)
|
||||
if self.immediate_action:
|
||||
self.immediate_action_cb(state_changed=True)
|
||||
elif c == util.KEY_CTRL_AND_ARROW_DOWN:
|
||||
if not self.allow_rearrange:
|
||||
return ret
|
||||
val = self.inputs.pop(self.active_input)
|
||||
self.active_input += 1
|
||||
self.inputs.insert(self.active_input, val)
|
||||
if self.immediate_action:
|
||||
self.immediate_action_cb(state_changed=True)
|
||||
else:
|
||||
ret = util.ReadState.IGNORED
|
||||
return ret
|
||||
|
||||
def get_values(self):
|
||||
vals = {}
|
||||
for i, ipt in enumerate(self.inputs):
|
||||
if not ipt.has_input():
|
||||
continue
|
||||
vals[ipt.name] = {"value": ipt.get_value(), "order": i, "active": self.active_input == i}
|
||||
return vals
|
||||
|
||||
def immediate_action_cb(self, state_changed=True):
|
||||
pass
|
||||
|
||||
def move_active(self, direction, amount):
|
||||
"""
|
||||
direction == -1: Up
|
||||
direction == 1: Down
|
||||
|
||||
"""
|
||||
self.last_lineoff_move = direction * amount
|
||||
|
||||
if direction > 0:
|
||||
if self.active_wrap:
|
||||
limit = self.active_input - 1
|
||||
if limit < 0:
|
||||
limit = len(self.inputs) + limit
|
||||
else:
|
||||
limit = len(self.inputs) - 1
|
||||
else:
|
||||
limit = 0
|
||||
if self.active_wrap:
|
||||
limit = self.active_input + 1
|
||||
|
||||
def next_move(nc, direction, limit):
|
||||
next_index = nc
|
||||
while next_index != limit:
|
||||
next_index += direction
|
||||
if direction > 0:
|
||||
next_index %= len(self.inputs)
|
||||
elif next_index < 0:
|
||||
next_index = len(self.inputs) + next_index
|
||||
|
||||
if self.inputs[next_index].selectable():
|
||||
return next_index
|
||||
if next_index == limit:
|
||||
return nc
|
||||
return nc
|
||||
|
||||
next_sel = self.active_input
|
||||
for a in range(amount):
|
||||
cur_sel = next_sel
|
||||
next_sel = next_move(next_sel, direction, limit)
|
||||
if cur_sel == next_sel:
|
||||
tot_height = self.get_content_height() + self.border_off_north + self.border_off_south
|
||||
if direction > 0:
|
||||
self.lineoff = min(self.lineoff + 1, tot_height - self.visible_content_pane_height)
|
||||
else:
|
||||
self.lineoff = max(self.lineoff - 1, 0)
|
||||
|
||||
if next_sel is not None:
|
||||
self.active_input = next_sel
|
||||
|
||||
def move_active_up(self, amount):
|
||||
self.move_active(-1, amount)
|
||||
if self.immediate_action:
|
||||
self.immediate_action_cb(state_changed=False)
|
||||
|
||||
def move_active_down(self, amount):
|
||||
self.move_active(1, amount)
|
||||
if self.immediate_action:
|
||||
self.immediate_action_cb(state_changed=False)
|
||||
|
||||
def get_content_height(self):
|
||||
height = 0
|
||||
for i, ipt in enumerate(self.inputs):
|
||||
if ipt.depend_skip():
|
||||
continue
|
||||
height += ipt.height
|
||||
return height
|
||||
|
||||
def ensure_active_visible(self):
|
||||
start_row = 0
|
||||
end_row = self.border_off_north
|
||||
for i, ipt in enumerate(self.inputs):
|
||||
if ipt.depend_skip():
|
||||
continue
|
||||
start_row = end_row
|
||||
end_row += ipt.height
|
||||
if i != self.active_input or not ipt.has_input():
|
||||
continue
|
||||
height = self.visible_content_pane_height
|
||||
if end_row > height + self.lineoff:
|
||||
self.lineoff += end_row - (height + self.lineoff) # Correct result depends on paranthesis
|
||||
elif start_row < self.lineoff:
|
||||
self.lineoff -= (self.lineoff - start_row)
|
||||
break
|
||||
|
||||
def render_inputs(self, focused=False):
|
||||
self._cursor_row = -1
|
||||
self._cursor_col = -1
|
||||
util.safe_curs_set(util.Curser.INVISIBLE)
|
||||
|
||||
self.ensure_active_visible()
|
||||
|
||||
crow = self.border_off_north
|
||||
for i, ipt in enumerate(self.inputs):
|
||||
if ipt.depend_skip():
|
||||
continue
|
||||
col = self.border_off_west
|
||||
field_width = self.width - self.border_off_east - self.border_off_west
|
||||
cursor_offset = self.border_off_west
|
||||
|
||||
if ipt.default_col != -1:
|
||||
default_col = int(ipt.default_col)
|
||||
if isinstance(ipt.default_col, basestring) and ipt.default_col[0] in ['+', '-']:
|
||||
col += default_col
|
||||
cursor_offset += default_col
|
||||
field_width -= default_col # Increase to col must be reflected here
|
||||
else:
|
||||
col = default_col
|
||||
crow += ipt.render(self.screen, crow, width=field_width, active=i == self.active_input,
|
||||
focused=focused, col=col, cursor_offset=cursor_offset)
|
||||
|
||||
if self._cursor_row >= 0:
|
||||
util.safe_curs_set(util.Curser.VERY_VISIBLE)
|
||||
move_cursor(self.screen, self._cursor_row, self._cursor_col)
|
349
deluge/ui/console/widgets/popup.py
Normal file
349
deluge/ui/console/widgets/popup.py
Normal file
@ -0,0 +1,349 @@
|
||||
# -*- 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.
|
||||
#
|
||||
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import logging
|
||||
|
||||
from deluge.decorators import overrides
|
||||
from deluge.ui.console.modes.basemode import InputKeyHandler
|
||||
from deluge.ui.console.utils import curses_util as util
|
||||
from deluge.ui.console.utils import format_utils
|
||||
from deluge.ui.console.widgets import BaseInputPane, BaseWindow
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ALIGN(object):
|
||||
TOP_LEFT = 1
|
||||
TOP_CENTER = 2
|
||||
TOP_RIGHT = 3
|
||||
MIDDLE_LEFT = 4
|
||||
MIDDLE_CENTER = 5
|
||||
MIDDLE_RIGHT = 6
|
||||
BOTTOM_LEFT = 7
|
||||
BOTTOM_CENTER = 8
|
||||
BOTTOM_RIGHT = 9
|
||||
DEFAULT = MIDDLE_CENTER
|
||||
|
||||
|
||||
class PopupsHandler(object):
|
||||
|
||||
def __init__(self):
|
||||
self._popups = []
|
||||
|
||||
@property
|
||||
def popup(self):
|
||||
if self._popups:
|
||||
return self._popups[-1]
|
||||
return None
|
||||
|
||||
def push_popup(self, pu, clear=False):
|
||||
if clear:
|
||||
self._popups = []
|
||||
self._popups.append(pu)
|
||||
|
||||
def pop_popup(self):
|
||||
if self.popup:
|
||||
return self._popups.pop()
|
||||
|
||||
def report_message(self, title, message):
|
||||
self.push_popup(MessagePopup(self, title, message))
|
||||
|
||||
|
||||
class Popup(BaseWindow, InputKeyHandler):
|
||||
|
||||
def __init__(self, parent_mode, title, width_req=0, height_req=0, align=ALIGN.DEFAULT,
|
||||
close_cb=None, encoding=None, base_popup=None, **kwargs):
|
||||
"""
|
||||
Init a new popup. The default constructor will handle sizing and borders and the like.
|
||||
|
||||
Args:
|
||||
parent_mode (basemode subclass): The mode which the popup will be drawn over
|
||||
title (str): the title of the popup window
|
||||
width_req (int or float): An integer value will be used as the width of the popup in character.
|
||||
A float value will indicate the requested ratio in relation to the
|
||||
parents screen width.
|
||||
height_req (int or float): An integer value will be used as the height of the popup in character.
|
||||
A float value will indicate the requested ratio in relation to the
|
||||
parents screen height.
|
||||
align (ALIGN): The alignment controlling the position of the popup on the screen.
|
||||
close_cb (func): Function to be called when the popup is closed
|
||||
encoding (str): The terminal encoding
|
||||
base_popup (Popup): A popup used to inherit width_req and height_req if not explicitly specified.
|
||||
|
||||
Note: The parent mode is responsible for calling refresh on any popups it wants to show.
|
||||
This should be called as the last thing in the parents refresh method.
|
||||
|
||||
The parent *must* also call read_input on the popup instead of/in addition to
|
||||
running its own read_input code if it wants to have the popup handle user input.
|
||||
|
||||
Popups have two methods that must be implemented:
|
||||
|
||||
refresh(self) - draw the popup window to screen. this default mode simply draws a bordered window
|
||||
with the supplied title to the screen
|
||||
|
||||
read_input(self) - handle user input to the popup.
|
||||
|
||||
"""
|
||||
InputKeyHandler.__init__(self)
|
||||
self.parent = parent_mode
|
||||
self.close_cb = close_cb
|
||||
self.height_req = height_req
|
||||
self.width_req = width_req
|
||||
self.align = align
|
||||
if base_popup:
|
||||
if not self.width_req:
|
||||
self.width_req = base_popup.width_req
|
||||
if not self.height_req:
|
||||
self.height_req = base_popup.height_req
|
||||
|
||||
hr, wr, posy, posx = self.calculate_size()
|
||||
BaseWindow.__init__(self, title, wr, hr, encoding=None)
|
||||
self.move_window(posy, posx)
|
||||
self._closed = False
|
||||
|
||||
@overrides(BaseWindow)
|
||||
def refresh(self):
|
||||
self.screen.erase()
|
||||
height = self.get_content_height()
|
||||
self.ensure_content_pane_height(height + self.border_off_north + self.border_off_south)
|
||||
BaseInputPane.render_inputs(self, focused=True)
|
||||
BaseWindow.refresh(self)
|
||||
|
||||
def calculate_size(self):
|
||||
|
||||
if isinstance(self.height_req, float) and 0.0 < self.height_req <= 1.0:
|
||||
height = int((self.parent.rows - 2) * self.height_req)
|
||||
else:
|
||||
height = self.height_req
|
||||
|
||||
if isinstance(self.width_req, float) and 0.0 < self.width_req <= 1.0:
|
||||
width = int((self.parent.cols - 2) * self.width_req)
|
||||
else:
|
||||
width = self.width_req
|
||||
|
||||
# Height
|
||||
if height == 0:
|
||||
height = int(self.parent.rows / 2)
|
||||
elif height == -1:
|
||||
height = self.parent.rows - 2
|
||||
elif height > self.parent.rows - 2:
|
||||
height = self.parent.rows - 2
|
||||
|
||||
# Width
|
||||
if width == 0:
|
||||
width = int(self.parent.cols / 2)
|
||||
elif width == -1:
|
||||
width = self.parent.cols
|
||||
elif width >= self.parent.cols:
|
||||
width = self.parent.cols
|
||||
|
||||
if self.align in [ALIGN.TOP_CENTER, ALIGN.TOP_LEFT, ALIGN.TOP_RIGHT]:
|
||||
begin_y = 1
|
||||
elif self.align in [ALIGN.MIDDLE_CENTER, ALIGN.MIDDLE_LEFT, ALIGN.MIDDLE_RIGHT]:
|
||||
begin_y = (self.parent.rows / 2) - (height / 2)
|
||||
elif self.align in [ALIGN.BOTTOM_CENTER, ALIGN.BOTTOM_LEFT, ALIGN.BOTTOM_RIGHT]:
|
||||
begin_y = self.parent.rows - height - 1
|
||||
|
||||
if self.align in [ALIGN.TOP_LEFT, ALIGN.MIDDLE_LEFT, ALIGN.BOTTOM_LEFT]:
|
||||
begin_x = 0
|
||||
elif self.align in [ALIGN.TOP_CENTER, ALIGN.MIDDLE_CENTER, ALIGN.BOTTOM_CENTER]:
|
||||
begin_x = (self.parent.cols / 2) - (width / 2)
|
||||
elif self.align in [ALIGN.TOP_RIGHT, ALIGN.MIDDLE_RIGHT, ALIGN.BOTTOM_RIGHT]:
|
||||
begin_x = self.parent.cols - width
|
||||
|
||||
return height, width, begin_y, begin_x
|
||||
|
||||
def handle_resize(self):
|
||||
height, width, begin_y, begin_x = self.calculate_size()
|
||||
self.resize_window(height, width)
|
||||
self.move_window(begin_y, begin_x)
|
||||
|
||||
def closed(self):
|
||||
return self._closed
|
||||
|
||||
def close(self, *args, **kwargs):
|
||||
self._closed = True
|
||||
if kwargs.get("call_cb", True):
|
||||
self._call_close_cb(*args)
|
||||
self.panel.hide()
|
||||
|
||||
def _call_close_cb(self, *args, **kwargs):
|
||||
if self.close_cb:
|
||||
self.close_cb(*args, base_popup=self, **kwargs)
|
||||
|
||||
@overrides(InputKeyHandler)
|
||||
def handle_read(self, c):
|
||||
if c == util.KEY_ESC: # close on esc, no action
|
||||
self.close(None)
|
||||
return util.ReadState.READ
|
||||
return util.ReadState.IGNORED
|
||||
|
||||
|
||||
class SelectablePopup(BaseInputPane, Popup):
|
||||
"""
|
||||
A popup which will let the user select from some of the lines that are added.
|
||||
"""
|
||||
def __init__(self, parent_mode, title, selection_cb, close_cb=None, input_cb=None,
|
||||
allow_rearrange=False, immediate_action=False, **kwargs):
|
||||
"""
|
||||
Args:
|
||||
parent_mode (basemode subclass): The mode which the popup will be drawn over
|
||||
title (str): the title of the popup window
|
||||
selection_cb (func): Function to be called on selection
|
||||
close_cb (func, optional): Function to be called when the popup is closed
|
||||
input_cb (func, optional): Function to be called on every keyboard input
|
||||
allow_rearrange (bool): Allow rearranging the selectable value
|
||||
immediate_action (bool): If immediate_action_cb should be called for every action
|
||||
kwargs (dict): Arguments passed to Popup
|
||||
|
||||
"""
|
||||
Popup.__init__(self, parent_mode, title, close_cb=close_cb, **kwargs)
|
||||
kwargs.update({"allow_rearrange": allow_rearrange, "immediate_action": immediate_action})
|
||||
BaseInputPane.__init__(self, self, **kwargs)
|
||||
self.selection_cb = selection_cb
|
||||
self.input_cb = input_cb
|
||||
self.hotkeys = {}
|
||||
self.cb_arg = {}
|
||||
self.cb_args = kwargs.get("cb_args", {})
|
||||
if "base_popup" not in self.cb_args:
|
||||
self.cb_args["base_popup"] = self
|
||||
|
||||
@property
|
||||
@overrides(BaseWindow)
|
||||
def visible_content_pane_height(self):
|
||||
"""We want to use the Popup property"""
|
||||
return Popup.visible_content_pane_height.fget(self)
|
||||
|
||||
def current_selection(self):
|
||||
"Returns a tuple of (selected index, selected data)"
|
||||
return self.active_input
|
||||
|
||||
def set_selection(self, index):
|
||||
"""Set a selected index"""
|
||||
self.active_input = index
|
||||
|
||||
def add_line(self, name, string, use_underline=True, cb_arg=None, foreground=None, selectable=True,
|
||||
selected=False, **kwargs):
|
||||
hotkey = None
|
||||
self.cb_arg[name] = cb_arg
|
||||
if use_underline:
|
||||
udx = string.find("_")
|
||||
if udx >= 0:
|
||||
hotkey = string[udx].lower()
|
||||
string = string[:udx] + "{!+underline!}" + string[udx + 1] + "{!-underline!}" + string[udx + 2:]
|
||||
|
||||
kwargs["selectable"] = selectable
|
||||
if foreground:
|
||||
kwargs["color_active"] = "%s,white" % foreground
|
||||
kwargs["color"] = "%s,black" % foreground
|
||||
|
||||
field = self.add_text_field(name, string, **kwargs)
|
||||
if hotkey:
|
||||
self.hotkeys[hotkey] = field
|
||||
|
||||
if selected:
|
||||
self.set_selection(len(self.inputs) - 1)
|
||||
|
||||
@overrides(Popup, BaseInputPane)
|
||||
def handle_read(self, c):
|
||||
if c in [curses.KEY_ENTER, util.KEY_ENTER2]:
|
||||
for k, v in self.get_values().iteritems():
|
||||
if v["active"]:
|
||||
if self.selection_cb(k, **dict(self.cb_args, data=self.cb_arg)):
|
||||
self.close(None)
|
||||
return util.ReadState.READ
|
||||
else:
|
||||
ret = BaseInputPane.handle_read(self, c)
|
||||
if ret != util.ReadState.IGNORED:
|
||||
return ret
|
||||
ret = Popup.handle_read(self, c)
|
||||
if ret != util.ReadState.IGNORED:
|
||||
if self.selection_cb(None):
|
||||
self.close(None)
|
||||
return ret
|
||||
|
||||
if self.input_cb:
|
||||
self.input_cb(c)
|
||||
|
||||
self.refresh()
|
||||
return util.ReadState.IGNORED
|
||||
|
||||
def add_divider(self, message=None, char="-", fill_width=True, color="white"):
|
||||
if message is not None:
|
||||
fill_width = False
|
||||
else:
|
||||
message = char
|
||||
self.add_divider_field("", message, selectable=False, fill_width=fill_width)
|
||||
|
||||
|
||||
class MessagePopup(Popup, BaseInputPane):
|
||||
"""
|
||||
Popup that just displays a message
|
||||
"""
|
||||
def __init__(self, parent_mode, title, message, align=ALIGN.DEFAULT,
|
||||
height_req=0.75, width_req=0.5, **kwargs):
|
||||
self.message = message
|
||||
Popup.__init__(self, parent_mode, title, align=align,
|
||||
height_req=height_req, width_req=width_req)
|
||||
BaseInputPane.__init__(self, self, immediate_action=True, **kwargs)
|
||||
lns = format_utils.wrap_string(self.message, self.width - 3, 3, True)
|
||||
|
||||
if isinstance(self.height_req, float):
|
||||
self.height_req = min(len(lns) + 2, int(parent_mode.rows * self.height_req))
|
||||
|
||||
self.handle_resize()
|
||||
self.no_refresh = False
|
||||
self.add_text_area("TextMessage", message)
|
||||
|
||||
@overrides(Popup, BaseInputPane)
|
||||
def handle_read(self, c):
|
||||
ret = BaseInputPane.handle_read(self, c)
|
||||
if ret != util.ReadState.IGNORED:
|
||||
return ret
|
||||
return Popup.handle_read(self, c)
|
||||
|
||||
|
||||
class InputPopup(Popup, BaseInputPane):
|
||||
|
||||
def __init__(self, parent_mode, title, **kwargs):
|
||||
Popup.__init__(self, parent_mode, title, **kwargs)
|
||||
BaseInputPane.__init__(self, self, **kwargs)
|
||||
# We need to replicate some things in order to wrap our inputs
|
||||
self.encoding = parent_mode.encoding
|
||||
|
||||
def _handle_callback(self, state_changed=True, close=True):
|
||||
self._call_close_cb(self.get_values(), state_changed=state_changed, close=close)
|
||||
|
||||
@overrides(BaseInputPane)
|
||||
def immediate_action_cb(self, state_changed=True):
|
||||
self._handle_callback(state_changed=state_changed, close=False)
|
||||
|
||||
@overrides(Popup, BaseInputPane)
|
||||
def handle_read(self, c):
|
||||
ret = BaseInputPane.handle_read(self, c)
|
||||
if ret != util.ReadState.IGNORED:
|
||||
return ret
|
||||
|
||||
if c in [curses.KEY_ENTER, util.KEY_ENTER2]:
|
||||
if self.close_cb:
|
||||
self._handle_callback(state_changed=False, close=False)
|
||||
util.safe_curs_set(util.Curser.INVISIBLE)
|
||||
return util.ReadState.READ
|
||||
elif c == util.KEY_ESC: # close on esc, no action
|
||||
self._handle_callback(state_changed=False, close=True)
|
||||
self.close(None)
|
||||
return util.ReadState.READ
|
||||
|
||||
self.refresh()
|
||||
return util.ReadState.READ
|
74
deluge/ui/console/widgets/sidebar.py
Normal file
74
deluge/ui/console/widgets/sidebar.py
Normal file
@ -0,0 +1,74 @@
|
||||
# -*- 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.
|
||||
#
|
||||
|
||||
import curses
|
||||
import logging
|
||||
|
||||
from deluge.decorators import overrides
|
||||
from deluge.ui.console.modes.basemode import add_string
|
||||
from deluge.ui.console.utils import curses_util as util
|
||||
from deluge.ui.console.widgets import BaseInputPane, BaseWindow
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Sidebar(BaseInputPane, BaseWindow):
|
||||
"""Base sidebar widget that handles choosing a selected widget
|
||||
with Up/Down arrows.
|
||||
|
||||
Shows the different states of the torrents and allows to filter the
|
||||
torrents based on state.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, torrentlist, width, height, title=None, allow_resize=False, **kwargs):
|
||||
BaseWindow.__init__(self, title, width, height, posy=1)
|
||||
BaseInputPane.__init__(self, self, immediate_action=True, **kwargs)
|
||||
self.parent = torrentlist
|
||||
self.focused = False
|
||||
self.allow_resize = allow_resize
|
||||
|
||||
def set_focused(self, focused):
|
||||
self.focused = focused
|
||||
|
||||
def has_focus(self):
|
||||
return self.focused and not self.hidden()
|
||||
|
||||
@overrides(BaseInputPane)
|
||||
def handle_read(self, c):
|
||||
if c == curses.KEY_UP:
|
||||
self.move_active_up(1)
|
||||
elif c == curses.KEY_DOWN:
|
||||
self.move_active_down(1)
|
||||
elif self.allow_resize and c in [ord('+'), ord('-')]:
|
||||
width = self.visible_content_pane_width + (1 if c == ord('+') else - 1)
|
||||
self.on_resize(width)
|
||||
else:
|
||||
return BaseInputPane.handle_read(self, c)
|
||||
return util.ReadState.READ
|
||||
|
||||
def on_resize(self, width):
|
||||
self.resize_window(self.height, width)
|
||||
|
||||
@overrides(BaseWindow)
|
||||
def refresh(self):
|
||||
height = self.get_content_height()
|
||||
self.ensure_content_pane_height(height + self.border_off_north + self.border_off_south)
|
||||
BaseInputPane.render_inputs(self, focused=self.has_focus())
|
||||
BaseWindow.refresh(self)
|
||||
|
||||
def _refresh(self):
|
||||
self.screen.erase()
|
||||
height = self.get_content_height()
|
||||
self.ensure_content_pane_height(height + self.border_off_north + self.border_off_south)
|
||||
BaseInputPane.render_inputs(self, focused=True)
|
||||
BaseWindow.refresh(self)
|
||||
|
||||
def add_string(self, row, string, scr=None, **kwargs):
|
||||
add_string(row, string, self.screen, self.parent.encoding, **kwargs)
|
152
deluge/ui/console/widgets/window.py
Normal file
152
deluge/ui/console/widgets/window.py
Normal file
@ -0,0 +1,152 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||
# 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.
|
||||
#
|
||||
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import logging
|
||||
|
||||
from deluge.ui.console.modes.basemode import add_string, mkpad, mkpanel
|
||||
from deluge.ui.console.utils.colors import get_color_pair
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseWindow(object):
|
||||
"""
|
||||
BaseWindow creates a curses screen to be used for showing panels and popup dialogs
|
||||
"""
|
||||
def __init__(self, title, width, height, posy=0, posx=0, encoding=None):
|
||||
"""
|
||||
Args:
|
||||
title (str): The title of the panel
|
||||
width (int): Width of the panel
|
||||
height (int): Height of the panel
|
||||
posy (int): Position of the panel's first row relative to the terminal screen
|
||||
posx (int): Position of the panel's first column relative to the terminal screen
|
||||
encoding (str): Terminal encoding
|
||||
"""
|
||||
self.title = title
|
||||
self.posy, self.posx = posy, posx
|
||||
if encoding is None:
|
||||
from deluge import component
|
||||
encoding = component.get("ConsoleUI").encoding
|
||||
self.encoding = encoding
|
||||
|
||||
self.panel = mkpanel(curses.COLOR_GREEN, height, width, posy, posx)
|
||||
self.outer_screen = self.panel.window()
|
||||
self.outer_screen.bkgdset(0, curses.COLOR_RED)
|
||||
by, bx = self.outer_screen.getbegyx()
|
||||
self.screen = mkpad(get_color_pair("white", "black"), height - 1, width - 2)
|
||||
self._height, self._width = self.outer_screen.getmaxyx()
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return self._height
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self._width
|
||||
|
||||
def add_string(self, row, string, scr=None, **kwargs):
|
||||
scr = scr if scr else self.screen
|
||||
add_string(row, string, scr, self.encoding, **kwargs)
|
||||
|
||||
def hide(self):
|
||||
self.panel.hide()
|
||||
|
||||
def show(self):
|
||||
self.panel.show()
|
||||
|
||||
def hidden(self):
|
||||
return self.panel.hidden()
|
||||
|
||||
def set_title(self, title):
|
||||
self.title = title
|
||||
|
||||
@property
|
||||
def visible_content_pane_size(self):
|
||||
y, x = self.outer_screen.getmaxyx()
|
||||
return (y - 2, x - 2)
|
||||
|
||||
@property
|
||||
def visible_content_pane_height(self):
|
||||
y, x = self.visible_content_pane_size
|
||||
return y
|
||||
|
||||
@property
|
||||
def visible_content_pane_width(self):
|
||||
y, x = self.visible_content_pane_size
|
||||
return x
|
||||
|
||||
def getmaxyx(self):
|
||||
return self.screen.getmaxyx()
|
||||
|
||||
def resize_window(self, rows, cols):
|
||||
self.outer_screen.resize(rows, cols)
|
||||
self.screen.resize(rows - 2, cols - 2)
|
||||
self._height, self._width = rows, cols
|
||||
|
||||
def move_window(self, posy, posx):
|
||||
self.outer_screen.mvwin(posy, posx)
|
||||
self.posy = posy
|
||||
self.posx = posx
|
||||
self._height, self._width = self.screen.getmaxyx()
|
||||
|
||||
def ensure_content_pane_height(self, height):
|
||||
max_y, max_x = self.screen.getmaxyx()
|
||||
if max_y < height:
|
||||
self.screen.resize(height, max_x)
|
||||
|
||||
def draw_scroll_indicator(self, screen):
|
||||
content_height = self.get_content_height()
|
||||
if content_height <= self.visible_content_pane_height:
|
||||
return
|
||||
|
||||
percent_scroll = float(self.lineoff) / (content_height - self.visible_content_pane_height)
|
||||
indicator_row = int(self.visible_content_pane_height * percent_scroll) + 1
|
||||
|
||||
# Never greater than height
|
||||
indicator_row = min(indicator_row, self.visible_content_pane_height)
|
||||
indicator_col = self.width + 1
|
||||
|
||||
add_string(indicator_row, "{!red,black,bold!}#", screen, self.encoding,
|
||||
col=indicator_col, pad=False, trim=False)
|
||||
|
||||
def refresh(self):
|
||||
height, width = self.visible_content_pane_size
|
||||
self.outer_screen.erase()
|
||||
self.outer_screen.border(0, 0, 0, 0)
|
||||
|
||||
if self.title:
|
||||
toff = max(1, (self.width // 2) - (len(self.title) // 2))
|
||||
self.add_string(0, "{!white,black,bold!}%s" % self.title, scr=self.outer_screen, col=toff, pad=False)
|
||||
|
||||
self.draw_scroll_indicator(self.outer_screen)
|
||||
self.outer_screen.noutrefresh()
|
||||
|
||||
try:
|
||||
# pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol
|
||||
# the p arguments refer to the upper left corner of the pad region to be displayed and
|
||||
# the s arguments define a clipping box on the screen within which the pad region is to be displayed.
|
||||
pminrow = self.lineoff
|
||||
pmincol = 0
|
||||
sminrow = self.posy + 1
|
||||
smincol = self.posx + 1
|
||||
smaxrow = height + self.posy
|
||||
smaxcol = width + self.posx
|
||||
self.screen.noutrefresh(pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol)
|
||||
except curses.error as ex:
|
||||
import traceback
|
||||
log.warn("Error on screen.noutrefresh(%s, %s, %s, %s, %s, %s) Error: '%s'\nStack: %s",
|
||||
pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol, ex, "".join(traceback.format_stack()))
|
Loading…
x
Reference in New Issue
Block a user