[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:
bendikro 2016-04-30 03:04:40 +02:00 committed by Calum Lind
parent 2f8b4732b4
commit 20bae1bf90
66 changed files with 6044 additions and 4984 deletions

View 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)

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -0,0 +1 @@
from deluge.ui.console.cmdline.command import BaseCommand # NOQA

View File

@ -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")

View File

@ -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):

View File

@ -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")

View File

@ -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")

View File

@ -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":

View File

@ -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()

View File

@ -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):

View File

@ -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")

View File

@ -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__,

View File

@ -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):

View File

@ -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")

View File

@ -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")

View File

@ -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")

View File

@ -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):

View File

@ -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")

View File

@ -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")

View File

@ -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)

View File

@ -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")

View File

@ -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):

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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()))

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -0,0 +1 @@
from deluge.ui.console.modes.preferences.preferences import Preferences # NOQA

View 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()

View 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]

View File

@ -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)

View File

@ -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()

View 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"

View 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)

View 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()

View 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)

View 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

View 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)

View 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)

View 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

View 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
View 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)

View File

View 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 = {

View 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

View 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

View File

@ -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

View 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

View 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

View 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)

View 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

View 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)

View 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()))