Merge branch 'newconsole'
Conflicts: deluge/ui/console/main.py
This commit is contained in:
commit
5f8eda9204
|
@ -61,7 +61,12 @@ schemes = {
|
||||||
"info": ("white", "black", "bold"),
|
"info": ("white", "black", "bold"),
|
||||||
"error": ("red", "black", "bold"),
|
"error": ("red", "black", "bold"),
|
||||||
"success": ("green", "black", "bold"),
|
"success": ("green", "black", "bold"),
|
||||||
"event": ("magenta", "black", "bold")
|
"event": ("magenta", "black", "bold"),
|
||||||
|
"selected": ("black", "white", "bold"),
|
||||||
|
"marked": ("white","blue","bold"),
|
||||||
|
"selectedmarked": ("blue","white","bold"),
|
||||||
|
"header": ("green","black","bold"),
|
||||||
|
"filterstatus": ("green", "blue", "bold")
|
||||||
}
|
}
|
||||||
|
|
||||||
# Colors for various torrent states
|
# Colors for various torrent states
|
||||||
|
@ -94,6 +99,14 @@ def init_colors():
|
||||||
curses.init_pair(counter, getattr(curses, fg), getattr(curses, bg))
|
curses.init_pair(counter, getattr(curses, fg), getattr(curses, bg))
|
||||||
counter += 1
|
counter += 1
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
pass
|
||||||
|
|
||||||
class BadColorString(Exception):
|
class BadColorString(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
#
|
||||||
|
# commander.py
|
||||||
|
#
|
||||||
|
# Copyright (C) 2008-2009 Ido Abramovich <ido.deluge@gmail.com>
|
||||||
|
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||||
|
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||||
|
#
|
||||||
|
# Deluge is free software.
|
||||||
|
#
|
||||||
|
# You may redistribute it and/or modify it under the terms of the
|
||||||
|
# GNU General Public License, as published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option)
|
||||||
|
# any later version.
|
||||||
|
#
|
||||||
|
# deluge is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
# See the GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with deluge. If not, write to:
|
||||||
|
# The Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin Street, Fifth Floor
|
||||||
|
# Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# In addition, as a special exception, the copyright holders give
|
||||||
|
# permission to link the code of portions of this program with the OpenSSL
|
||||||
|
# library.
|
||||||
|
# You must obey the GNU General Public License in all respects for all of
|
||||||
|
# the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
# exception, you may extend this exception to your version of the file(s),
|
||||||
|
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||||
|
# this exception statement from your version. If you delete this exception
|
||||||
|
# statement from all source files in the program, then also delete it here.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
from twisted.internet import defer, reactor
|
||||||
|
import deluge.component as component
|
||||||
|
from deluge.ui.client import client
|
||||||
|
from deluge.ui.console import UI_PATH
|
||||||
|
from colors import strip_colors
|
||||||
|
|
||||||
|
import logging
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class Commander:
|
||||||
|
def __init__(self, cmds):
|
||||||
|
self._commands = cmds
|
||||||
|
self.console = component.get("ConsoleUI")
|
||||||
|
|
||||||
|
def write(self,line):
|
||||||
|
print(strip_colors(line))
|
||||||
|
|
||||||
|
def do_command(self, cmd):
|
||||||
|
"""
|
||||||
|
Processes a command.
|
||||||
|
|
||||||
|
:param cmd: str, the command string
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not cmd:
|
||||||
|
return
|
||||||
|
cmd, _, line = cmd.partition(' ')
|
||||||
|
try:
|
||||||
|
parser = self._commands[cmd].create_parser()
|
||||||
|
except KeyError:
|
||||||
|
self.write("{!error!}Unknown command: %s" % cmd)
|
||||||
|
return
|
||||||
|
args = self._commands[cmd].split(line)
|
||||||
|
|
||||||
|
# Do a little hack here to print 'command --help' properly
|
||||||
|
parser._print_help = parser.print_help
|
||||||
|
def print_help(f=None):
|
||||||
|
if self.interactive:
|
||||||
|
self.write(parser.format_help())
|
||||||
|
else:
|
||||||
|
parser._print_help(f)
|
||||||
|
parser.print_help = print_help
|
||||||
|
|
||||||
|
# Only these commands can be run when not connected to a daemon
|
||||||
|
not_connected_cmds = ["help", "connect", "quit"]
|
||||||
|
aliases = []
|
||||||
|
for c in not_connected_cmds:
|
||||||
|
aliases.extend(self._commands[c].aliases)
|
||||||
|
not_connected_cmds.extend(aliases)
|
||||||
|
|
||||||
|
if not client.connected() and cmd not in not_connected_cmds:
|
||||||
|
self.write("{!error!}Not connected to a daemon, please use the connect command first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
options, args = parser.parse_args(args)
|
||||||
|
except Exception, e:
|
||||||
|
self.write("{!error!}Error parsing options: %s" % e)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not getattr(options, '_exit', False):
|
||||||
|
try:
|
||||||
|
ret = self._commands[cmd].handle(*args, **options.__dict__)
|
||||||
|
except Exception, e:
|
||||||
|
self.write("{!error!}" + str(e))
|
||||||
|
log.exception(e)
|
||||||
|
import traceback
|
||||||
|
self.write("%s" % traceback.format_exc())
|
||||||
|
return defer.succeed(True)
|
||||||
|
else:
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def exec_args(self,args,host,port,username,password):
|
||||||
|
def on_connect(result):
|
||||||
|
def on_started(result):
|
||||||
|
def on_started(result):
|
||||||
|
deferreds = []
|
||||||
|
# If we have args, lets process them and quit
|
||||||
|
# allow multiple commands split by ";"
|
||||||
|
for arg in args.split(";"):
|
||||||
|
deferreds.append(defer.maybeDeferred(self.do_command, arg.strip()))
|
||||||
|
|
||||||
|
def on_complete(result):
|
||||||
|
self.do_command("quit")
|
||||||
|
|
||||||
|
dl = defer.DeferredList(deferreds).addCallback(on_complete)
|
||||||
|
|
||||||
|
# We need to wait for the rpcs in start() to finish before processing
|
||||||
|
# any of the commands.
|
||||||
|
self.console.started_deferred.addCallback(on_started)
|
||||||
|
component.start().addCallback(on_started)
|
||||||
|
|
||||||
|
def on_connect_fail(result):
|
||||||
|
from deluge.ui.client import DelugeRPCError
|
||||||
|
if isinstance(result.value,DelugeRPCError):
|
||||||
|
rm = result.value.exception_msg
|
||||||
|
else:
|
||||||
|
rm = result.getErrorMessage()
|
||||||
|
print "Could not connect to: %s:%d\n %s"%(host,port,rm)
|
||||||
|
self.do_command("quit")
|
||||||
|
|
||||||
|
if host:
|
||||||
|
d = client.connect(host,port,username,password)
|
||||||
|
else:
|
||||||
|
d = client.connect()
|
||||||
|
d.addCallback(on_connect)
|
||||||
|
d.addErrback(on_connect_fail)
|
||||||
|
|
|
@ -47,4 +47,6 @@ class Command(BaseCommand):
|
||||||
for key, value in status.items():
|
for key, value in status.items():
|
||||||
self.console.write("{!info!}%s: {!input!}%s" % (key, value))
|
self.console.write("{!info!}%s: {!input!}%s" % (key, value))
|
||||||
|
|
||||||
client.core.get_cache_status().addCallback(on_cache_status)
|
d = client.core.get_cache_status()
|
||||||
|
d.addCallback(on_cache_status)
|
||||||
|
return d
|
||||||
|
|
|
@ -44,7 +44,7 @@ import deluge.component as component
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
"""Enable and disable debugging"""
|
"""Enable and disable debugging"""
|
||||||
usage = 'debug [on|off]'
|
usage = 'Usage: debug [on|off]'
|
||||||
def handle(self, state='', **options):
|
def handle(self, state='', **options):
|
||||||
if state == 'on':
|
if state == 'on':
|
||||||
deluge.log.setLoggerLevel("debug")
|
deluge.log.setLoggerLevel("debug")
|
||||||
|
|
|
@ -39,7 +39,8 @@ from deluge.ui.client import client
|
||||||
import deluge.component as component
|
import deluge.component as component
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
"Shutdown the deluge server."
|
"Shutdown the deluge server"
|
||||||
|
usage = "Usage: halt"
|
||||||
def handle(self, **options):
|
def handle(self, **options):
|
||||||
self.console = component.get("ConsoleUI")
|
self.console = component.get("ConsoleUI")
|
||||||
|
|
||||||
|
|
|
@ -85,7 +85,7 @@ def format_progressbar(progress, width):
|
||||||
s = "["
|
s = "["
|
||||||
p = int(round((progress/100) * w))
|
p = int(round((progress/100) * w))
|
||||||
s += "#" * p
|
s += "#" * p
|
||||||
s += "~" * (w - p)
|
s += "-" * (w - p)
|
||||||
s += "]"
|
s += "]"
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,7 @@ from twisted.internet import reactor
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
"""Exit from the client."""
|
"""Exit from the client."""
|
||||||
aliases = ['exit']
|
aliases = ['exit']
|
||||||
|
interactive_only = True
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
if client.connected():
|
if client.connected():
|
||||||
def on_disconnect(result):
|
def on_disconnect(result):
|
||||||
|
|
|
@ -43,16 +43,17 @@ import locale
|
||||||
|
|
||||||
from twisted.internet import defer, reactor
|
from twisted.internet import defer, reactor
|
||||||
|
|
||||||
from deluge.ui.console import UI_PATH
|
|
||||||
import deluge.component as component
|
import deluge.component as component
|
||||||
from deluge.ui.client import client
|
from deluge.ui.client import client
|
||||||
import deluge.common
|
import deluge.common
|
||||||
from deluge.ui.coreconfig import CoreConfig
|
from deluge.ui.coreconfig import CoreConfig
|
||||||
|
from deluge.ui.sessionproxy import SessionProxy
|
||||||
from deluge.ui.console.statusbars import StatusBars
|
from deluge.ui.console.statusbars import StatusBars
|
||||||
from deluge.ui.console.eventlog import EventLog
|
from deluge.ui.console.eventlog import EventLog
|
||||||
import screen
|
#import screen
|
||||||
import colors
|
import colors
|
||||||
from deluge.ui.ui import _UI
|
from deluge.ui.ui import _UI
|
||||||
|
from deluge.ui.console import UI_PATH
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -62,16 +63,62 @@ class Console(_UI):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(Console, self).__init__("console")
|
super(Console, self).__init__("console")
|
||||||
cmds = load_commands(os.path.join(UI_PATH, 'commands'))
|
group = optparse.OptionGroup(self.parser, "Console Options","These options control how "
|
||||||
|
"the console connects to the daemon. These options will be "
|
||||||
|
"used if you pass a command, or if you have autoconnect "
|
||||||
|
"enabled for the console ui.")
|
||||||
|
|
||||||
group = optparse.OptionGroup(self.parser, "Console Commands",
|
group.add_option("-d","--daemon",dest="daemon_addr",
|
||||||
"\n".join(cmds.keys()))
|
action="store",type="str",default="127.0.0.1",
|
||||||
|
help="Set the address of the daemon to connect to."
|
||||||
|
" [default: %default]")
|
||||||
|
group.add_option("-p","--port",dest="daemon_port",
|
||||||
|
help="Set the port to connect to the daemon on. [default: %default]",
|
||||||
|
action="store",type="int",default=58846)
|
||||||
|
group.add_option("-u","--username",dest="daemon_user",
|
||||||
|
help="Set the username to connect to the daemon with. [default: %default]",
|
||||||
|
action="store",type="string")
|
||||||
|
group.add_option("-P","--password",dest="daemon_pass",
|
||||||
|
help="Set the password to connect to the daemon with. [default: %default]",
|
||||||
|
action="store",type="string")
|
||||||
self.parser.add_option_group(group)
|
self.parser.add_option_group(group)
|
||||||
|
|
||||||
|
self.cmds = load_commands(os.path.join(UI_PATH, 'commands'))
|
||||||
|
class CommandOptionGroup(optparse.OptionGroup):
|
||||||
|
def __init__(self, parser, title, description=None, cmds = None):
|
||||||
|
optparse.OptionGroup.__init__(self,parser,title,description)
|
||||||
|
self.cmds = cmds
|
||||||
|
|
||||||
|
def format_help(self, formatter):
|
||||||
|
result = formatter.format_heading(self.title)
|
||||||
|
formatter.indent()
|
||||||
|
if self.description:
|
||||||
|
result += "%s\n"%formatter.format_description(self.description)
|
||||||
|
for cname in self.cmds:
|
||||||
|
cmd = self.cmds[cname]
|
||||||
|
if cmd.interactive_only or cname in cmd.aliases: continue
|
||||||
|
allnames = [cname]
|
||||||
|
allnames.extend(cmd.aliases)
|
||||||
|
cname = "/".join(allnames)
|
||||||
|
result += formatter.format_heading(" - ".join([cname,cmd.__doc__]))
|
||||||
|
formatter.indent()
|
||||||
|
result += "%*s%s\n" % (formatter.current_indent, "", cmd.usage)
|
||||||
|
formatter.dedent()
|
||||||
|
formatter.dedent()
|
||||||
|
return result
|
||||||
|
cmd_group = CommandOptionGroup(self.parser, "Console Commands",
|
||||||
|
description="The following commands can be issued at the "
|
||||||
|
"command line. Commands should be quoted, so, for example, "
|
||||||
|
"to pause torrent with id 'abc' you would run: '%s "
|
||||||
|
"\"pause abc\"'"%os.path.basename(sys.argv[0]),
|
||||||
|
cmds=self.cmds)
|
||||||
|
self.parser.add_option_group(cmd_group)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
super(Console, self).start()
|
super(Console, self).start()
|
||||||
|
ConsoleUI(self.args,self.cmds,(self.options.daemon_addr,
|
||||||
ConsoleUI(self.args)
|
self.options.daemon_port,self.options.daemon_user,
|
||||||
|
self.options.daemon_pass))
|
||||||
|
|
||||||
def start():
|
def start():
|
||||||
Console().start()
|
Console().start()
|
||||||
|
@ -92,9 +139,11 @@ class OptionParser(optparse.OptionParser):
|
||||||
"""
|
"""
|
||||||
raise Exception(msg)
|
raise Exception(msg)
|
||||||
|
|
||||||
|
|
||||||
class BaseCommand(object):
|
class BaseCommand(object):
|
||||||
|
|
||||||
usage = 'usage'
|
usage = 'usage'
|
||||||
|
interactive_only = False
|
||||||
option_list = tuple()
|
option_list = tuple()
|
||||||
aliases = []
|
aliases = []
|
||||||
|
|
||||||
|
@ -122,6 +171,7 @@ class BaseCommand(object):
|
||||||
epilog = self.epilog,
|
epilog = self.epilog,
|
||||||
option_list = self.option_list)
|
option_list = self.option_list)
|
||||||
|
|
||||||
|
|
||||||
def load_commands(command_dir, exclude=[]):
|
def load_commands(command_dir, exclude=[]):
|
||||||
def get_command(name):
|
def get_command(name):
|
||||||
return getattr(__import__('deluge.ui.console.commands.%s' % name, {}, {}, ['Command']), 'Command')()
|
return getattr(__import__('deluge.ui.console.commands.%s' % name, {}, {}, ['Command']), 'Command')()
|
||||||
|
@ -142,11 +192,13 @@ def load_commands(command_dir, exclude=[]):
|
||||||
except OSError, e:
|
except OSError, e:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
class ConsoleUI(component.Component):
|
class ConsoleUI(component.Component):
|
||||||
def __init__(self, args=None):
|
def __init__(self, args=None, cmds = None, daemon = None):
|
||||||
component.Component.__init__(self, "ConsoleUI", 2)
|
component.Component.__init__(self, "ConsoleUI", 2)
|
||||||
|
|
||||||
self.batch_write = False
|
# keep track of events for the log view
|
||||||
|
self.events = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
locale.setlocale(locale.LC_ALL, '')
|
locale.setlocale(locale.LC_ALL, '')
|
||||||
|
@ -155,8 +207,10 @@ class ConsoleUI(component.Component):
|
||||||
self.encoding = sys.getdefaultencoding()
|
self.encoding = sys.getdefaultencoding()
|
||||||
|
|
||||||
log.debug("Using encoding: %s", self.encoding)
|
log.debug("Using encoding: %s", self.encoding)
|
||||||
# Load all the commands
|
|
||||||
self._commands = load_commands(os.path.join(UI_PATH, 'commands'))
|
|
||||||
|
# start up the session proxy
|
||||||
|
self.sessionproxy = SessionProxy()
|
||||||
|
|
||||||
client.set_disconnect_callback(self.on_client_disconnect)
|
client.set_disconnect_callback(self.on_client_disconnect)
|
||||||
|
|
||||||
|
@ -165,30 +219,18 @@ class ConsoleUI(component.Component):
|
||||||
if args:
|
if args:
|
||||||
args = args[0]
|
args = args[0]
|
||||||
self.interactive = False
|
self.interactive = False
|
||||||
|
if not cmds:
|
||||||
# Try to connect to the localhost daemon
|
print "Sorry, couldn't find any commands"
|
||||||
def on_connect(result):
|
return
|
||||||
def on_started(result):
|
else:
|
||||||
if not self.interactive:
|
self._commands = cmds
|
||||||
def on_started(result):
|
from commander import Commander
|
||||||
deferreds = []
|
cmdr = Commander(cmds)
|
||||||
# If we have args, lets process them and quit
|
if daemon:
|
||||||
# allow multiple commands split by ";"
|
cmdr.exec_args(args,*daemon)
|
||||||
for arg in args.split(";"):
|
else:
|
||||||
deferreds.append(defer.maybeDeferred(self.do_command, arg.strip()))
|
cmdr.exec_args(args,None,None,None,None)
|
||||||
|
|
||||||
def on_complete(result):
|
|
||||||
self.do_command("quit")
|
|
||||||
|
|
||||||
dl = defer.DeferredList(deferreds).addCallback(on_complete)
|
|
||||||
|
|
||||||
# We need to wait for the rpcs in start() to finish before processing
|
|
||||||
# any of the commands.
|
|
||||||
self.started_deferred.addCallback(on_started)
|
|
||||||
component.start().addCallback(on_started)
|
|
||||||
|
|
||||||
d = client.connect()
|
|
||||||
d.addCallback(on_connect)
|
|
||||||
|
|
||||||
self.coreconfig = CoreConfig()
|
self.coreconfig = CoreConfig()
|
||||||
if self.interactive and not deluge.common.windows_check():
|
if self.interactive and not deluge.common.windows_check():
|
||||||
|
@ -218,8 +260,9 @@ Please use commands from the command line, eg:\n
|
||||||
# We want to do an interactive session, so start up the curses screen and
|
# We want to do an interactive session, so start up the curses screen and
|
||||||
# pass it the function that handles commands
|
# pass it the function that handles commands
|
||||||
colors.init_colors()
|
colors.init_colors()
|
||||||
self.screen = screen.Screen(stdscr, self.do_command, self.tab_completer, self.encoding)
|
|
||||||
self.statusbars = StatusBars()
|
self.statusbars = StatusBars()
|
||||||
|
from modes.connectionmanager import ConnectionManager
|
||||||
|
self.screen = ConnectionManager(stdscr, self.encoding)
|
||||||
self.eventlog = EventLog()
|
self.eventlog = EventLog()
|
||||||
|
|
||||||
self.screen.topbar = "{!status!}Deluge " + deluge.common.get_version() + " Console"
|
self.screen.topbar = "{!status!}Deluge " + deluge.common.get_version() + " Console"
|
||||||
|
@ -233,202 +276,22 @@ Please use commands from the command line, eg:\n
|
||||||
# Start the twisted mainloop
|
# Start the twisted mainloop
|
||||||
reactor.run()
|
reactor.run()
|
||||||
|
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
# 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
|
# Maintain a list of (torrent_id, name) for use in tab completion
|
||||||
self.torrents = []
|
if not self.interactive:
|
||||||
def on_session_state(result):
|
self.started_deferred = defer.Deferred()
|
||||||
def on_torrents_status(torrents):
|
self.torrents = []
|
||||||
for torrent_id, status in torrents.items():
|
def on_session_state(result):
|
||||||
self.torrents.append((torrent_id, status["name"]))
|
def on_torrents_status(torrents):
|
||||||
self.started_deferred.callback(True)
|
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_torrents_status({"id": result}, ["name"]).addCallback(on_torrents_status)
|
||||||
client.core.get_session_state().addCallback(on_session_state)
|
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)
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def set_batch_write(self, batch):
|
|
||||||
"""
|
|
||||||
When this is set the screen is not refreshed after a `:meth:write` until
|
|
||||||
this is set to False.
|
|
||||||
|
|
||||||
:param batch: set True to prevent screen refreshes after a `:meth:write`
|
|
||||||
:type batch: bool
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.batch_write = batch
|
|
||||||
if not batch and self.interactive:
|
|
||||||
self.screen.refresh()
|
|
||||||
|
|
||||||
def write(self, line):
|
|
||||||
"""
|
|
||||||
Writes a line out depending on if we're in interactive mode or not.
|
|
||||||
|
|
||||||
:param line: str, the line to print
|
|
||||||
|
|
||||||
"""
|
|
||||||
if self.interactive:
|
|
||||||
self.screen.add_line(line, not self.batch_write)
|
|
||||||
else:
|
|
||||||
print(colors.strip_colors(line.encode("utf-8")))
|
|
||||||
|
|
||||||
def do_command(self, cmd):
|
|
||||||
"""
|
|
||||||
Processes a command.
|
|
||||||
|
|
||||||
:param cmd: str, the command string
|
|
||||||
|
|
||||||
"""
|
|
||||||
if not cmd:
|
|
||||||
return
|
|
||||||
cmd, _, line = cmd.partition(' ')
|
|
||||||
try:
|
|
||||||
parser = self._commands[cmd].create_parser()
|
|
||||||
except KeyError:
|
|
||||||
self.write("{!error!}Unknown command: %s" % cmd)
|
|
||||||
return
|
|
||||||
args = self._commands[cmd].split(line)
|
|
||||||
|
|
||||||
# Do a little hack here to print 'command --help' properly
|
|
||||||
parser._print_help = parser.print_help
|
|
||||||
def print_help(f=None):
|
|
||||||
if self.interactive:
|
|
||||||
self.write(parser.format_help())
|
|
||||||
else:
|
|
||||||
parser._print_help(f)
|
|
||||||
parser.print_help = print_help
|
|
||||||
|
|
||||||
# Only these commands can be run when not connected to a daemon
|
|
||||||
not_connected_cmds = ["help", "connect", "quit"]
|
|
||||||
aliases = []
|
|
||||||
for c in not_connected_cmds:
|
|
||||||
aliases.extend(self._commands[c].aliases)
|
|
||||||
not_connected_cmds.extend(aliases)
|
|
||||||
|
|
||||||
if not client.connected() and cmd not in not_connected_cmds:
|
|
||||||
self.write("{!error!}Not connected to a daemon, please use the connect command first.")
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
options, args = parser.parse_args(args)
|
|
||||||
except Exception, e:
|
|
||||||
self.write("{!error!}Error parsing options: %s" % e)
|
|
||||||
return
|
|
||||||
|
|
||||||
if not getattr(options, '_exit', False):
|
|
||||||
try:
|
|
||||||
ret = self._commands[cmd].handle(*args, **options.__dict__)
|
|
||||||
except Exception, e:
|
|
||||||
self.write("{!error!}" + str(e))
|
|
||||||
log.exception(e)
|
|
||||||
import traceback
|
|
||||||
self.write("%s" % traceback.format_exc())
|
|
||||||
return defer.succeed(True)
|
|
||||||
else:
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def tab_completer(self, line, cursor, second_hit):
|
|
||||||
"""
|
|
||||||
Called when the user hits 'tab' and will autocomplete or show options.
|
|
||||||
If a command is already supplied in the line, this function will call the
|
|
||||||
complete method of the command.
|
|
||||||
|
|
||||||
:param line: str, the current input string
|
|
||||||
:param cursor: int, the cursor position in the line
|
|
||||||
:param second_hit: bool, if this is the second time in a row the tab key
|
|
||||||
has been pressed
|
|
||||||
|
|
||||||
:returns: 2-tuple (string, cursor position)
|
|
||||||
|
|
||||||
"""
|
|
||||||
# First check to see if there is no space, this will mean that it's a
|
|
||||||
# command that needs to be completed.
|
|
||||||
if " " not in line:
|
|
||||||
possible_matches = []
|
|
||||||
# Iterate through the commands looking for ones that startwith the
|
|
||||||
# line.
|
|
||||||
for cmd in self._commands:
|
|
||||||
if cmd.startswith(line):
|
|
||||||
possible_matches.append(cmd + " ")
|
|
||||||
|
|
||||||
line_prefix = ""
|
|
||||||
else:
|
|
||||||
cmd = line.split(" ")[0]
|
|
||||||
if cmd in self._commands:
|
|
||||||
# Call the command's complete method to get 'er done
|
|
||||||
possible_matches = self._commands[cmd].complete(line.split(" ")[-1])
|
|
||||||
line_prefix = " ".join(line.split(" ")[:-1]) + " "
|
|
||||||
else:
|
|
||||||
# This is a bogus command
|
|
||||||
return (line, cursor)
|
|
||||||
|
|
||||||
# No matches, so just return what we got passed
|
|
||||||
if len(possible_matches) == 0:
|
|
||||||
return (line, cursor)
|
|
||||||
# If we only have 1 possible match, then just modify the line and
|
|
||||||
# return it, else we need to print out the matches without modifying
|
|
||||||
# the line.
|
|
||||||
elif len(possible_matches) == 1:
|
|
||||||
new_line = line_prefix + possible_matches[0]
|
|
||||||
return (new_line, len(new_line))
|
|
||||||
else:
|
|
||||||
if second_hit:
|
|
||||||
# Only print these out if it's a second_hit
|
|
||||||
self.write(" ")
|
|
||||||
for match in possible_matches:
|
|
||||||
self.write(match)
|
|
||||||
else:
|
|
||||||
p = " ".join(line.split(" ")[:-1])
|
|
||||||
new_line = " ".join([p, os.path.commonprefix(possible_matches)])
|
|
||||||
if len(new_line) > len(line):
|
|
||||||
line = new_line
|
|
||||||
cursor = len(line)
|
|
||||||
return (line, cursor)
|
|
||||||
|
|
||||||
def tab_complete_torrent(self, line):
|
|
||||||
"""
|
|
||||||
Completes torrent_ids or names.
|
|
||||||
|
|
||||||
:param line: str, the string to complete
|
|
||||||
|
|
||||||
:returns: list of matches
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
possible_matches = []
|
|
||||||
|
|
||||||
# Find all possible matches
|
|
||||||
for torrent_id, torrent_name in self.torrents:
|
|
||||||
if torrent_id.startswith(line):
|
|
||||||
possible_matches.append(torrent_id + " ")
|
|
||||||
if torrent_name.startswith(line):
|
|
||||||
possible_matches.append(torrent_name + " ")
|
|
||||||
|
|
||||||
return possible_matches
|
|
||||||
|
|
||||||
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):
|
def match_torrent(self, string):
|
||||||
"""
|
"""
|
||||||
Returns a list of torrent_id matches for the string. It will search both
|
Returns a list of torrent_id matches for the string. It will search both
|
||||||
|
@ -447,15 +310,33 @@ Please use commands from the command line, eg:\n
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def on_torrent_added_event(self, event):
|
|
||||||
def on_torrent_status(status):
|
|
||||||
self.torrents.append((event.torrent_id, status["name"]))
|
|
||||||
client.core.get_torrent_status(event.torrent_id, ["name"]).addCallback(on_torrent_status)
|
|
||||||
|
|
||||||
def on_torrent_removed_event(self, event):
|
def get_torrent_name(self, torrent_id):
|
||||||
for index, (tid, name) in enumerate(self.torrents):
|
if self.interactive and hasattr(self.screen,"get_torrent_name"):
|
||||||
if event.torrent_id == tid:
|
return self.screen.get_torrent_name(torrent_id)
|
||||||
del self.torrents[index]
|
|
||||||
|
for tid, name in self.torrents:
|
||||||
|
if torrent_id == tid:
|
||||||
|
return name
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def set_batch_write(self, batch):
|
||||||
|
# only kept for legacy reasons, don't actually do anything
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_mode(self, mode):
|
||||||
|
reactor.removeReader(self.screen)
|
||||||
|
self.screen = mode
|
||||||
|
self.statusbars.screen = self.screen
|
||||||
|
reactor.addReader(self.screen)
|
||||||
|
|
||||||
def on_client_disconnect(self):
|
def on_client_disconnect(self):
|
||||||
component.stop()
|
component.stop()
|
||||||
|
|
||||||
|
def write(self, s):
|
||||||
|
if self.interactive:
|
||||||
|
self.events.append(s)
|
||||||
|
else:
|
||||||
|
print colors.strip_colors(s)
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
#
|
||||||
|
# add_util.py
|
||||||
|
#
|
||||||
|
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||||
|
#
|
||||||
|
# Modified function from commands/add.py:
|
||||||
|
# Copyright (C) 2008-2009 Ido Abramovich <ido.deluge@gmail.com>
|
||||||
|
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||||
|
#
|
||||||
|
# Deluge is free software.
|
||||||
|
#
|
||||||
|
# You may redistribute it and/or modify it under the terms of the
|
||||||
|
# GNU General Public License, as published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option)
|
||||||
|
# any later version.
|
||||||
|
#
|
||||||
|
# deluge is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
# See the GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with deluge. If not, write to:
|
||||||
|
# The Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin Street, Fifth Floor
|
||||||
|
# Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# In addition, as a special exception, the copyright holders give
|
||||||
|
# permission to link the code of portions of this program with the OpenSSL
|
||||||
|
# library.
|
||||||
|
# You must obey the GNU General Public License in all respects for all of
|
||||||
|
# the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
# exception, you may extend this exception to your version of the file(s),
|
||||||
|
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||||
|
# this exception statement from your version. If you delete this exception
|
||||||
|
# statement from all source files in the program, then also delete it here.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
from twisted.internet import defer
|
||||||
|
|
||||||
|
from deluge.ui.client import client
|
||||||
|
import deluge.component as component
|
||||||
|
|
||||||
|
from optparse import make_option
|
||||||
|
import os,base64,glob
|
||||||
|
|
||||||
|
try:
|
||||||
|
import libtorrent
|
||||||
|
add_get_info = libtorrent.torrent_info
|
||||||
|
except:
|
||||||
|
add_get_info = deluge.ui.common.TorrentInfo
|
||||||
|
|
||||||
|
def add_torrent(t_file, options, success_cb, fail_cb, ress):
|
||||||
|
t_options = {}
|
||||||
|
if options["path"]:
|
||||||
|
t_options["download_location"] = os.path.expanduser(options["path"])
|
||||||
|
t_options["add_paused"] = options["add_paused"]
|
||||||
|
|
||||||
|
files = glob.glob(t_file)
|
||||||
|
num_files = len(files)
|
||||||
|
ress["total"] = num_files
|
||||||
|
|
||||||
|
if num_files <= 0:
|
||||||
|
fail_cb("Doesn't exist",t_file,ress)
|
||||||
|
|
||||||
|
for f in files:
|
||||||
|
if not os.path.exists(f):
|
||||||
|
fail_cb("Doesn't exist",f,ress)
|
||||||
|
continue
|
||||||
|
if not os.path.isfile(f):
|
||||||
|
fail_cb("Is a directory",f,ress)
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
add_get_info(f)
|
||||||
|
except Exception as e:
|
||||||
|
fail_cb(e.message,f,ress)
|
||||||
|
continue
|
||||||
|
|
||||||
|
filename = os.path.split(f)[-1]
|
||||||
|
filedump = base64.encodestring(open(f).read())
|
||||||
|
|
||||||
|
client.core.add_torrent_file(filename, filedump, t_options).addCallback(success_cb,f,ress).addErrback(fail_cb,f,ress)
|
||||||
|
|
|
@ -0,0 +1,754 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# alltorrens.py
|
||||||
|
#
|
||||||
|
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||||
|
#
|
||||||
|
# Deluge is free software.
|
||||||
|
#
|
||||||
|
# You may redistribute it and/or modify it under the terms of the
|
||||||
|
# GNU General Public License, as published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option)
|
||||||
|
# any later version.
|
||||||
|
#
|
||||||
|
# deluge is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
# See the GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with deluge. If not, write to:
|
||||||
|
# The Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin Street, Fifth Floor
|
||||||
|
# Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# In addition, as a special exception, the copyright holders give
|
||||||
|
# permission to link the code of portions of this program with the OpenSSL
|
||||||
|
# library.
|
||||||
|
# You must obey the GNU General Public License in all respects for all of
|
||||||
|
# the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
# exception, you may extend this exception to your version of the file(s),
|
||||||
|
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||||
|
# this exception statement from your version. If you delete this exception
|
||||||
|
# statement from all source files in the program, then also delete it here.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
import deluge.component as component
|
||||||
|
from basemode import BaseMode
|
||||||
|
import deluge.common
|
||||||
|
from deluge.ui.client import client
|
||||||
|
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
from deluge.ui.sessionproxy import SessionProxy
|
||||||
|
|
||||||
|
from popup import Popup,SelectablePopup,MessagePopup
|
||||||
|
from add_util import add_torrent
|
||||||
|
from input_popup import InputPopup
|
||||||
|
from torrentdetail import TorrentDetail
|
||||||
|
from preferences import Preferences
|
||||||
|
from torrent_actions import torrent_actions_popup
|
||||||
|
from eventview import EventView
|
||||||
|
|
||||||
|
import format_utils
|
||||||
|
|
||||||
|
try:
|
||||||
|
import curses
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
import logging
|
||||||
|
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 by having a white background.
|
||||||
|
You can change the selected torrent using the up/down arrows or the
|
||||||
|
PgUp/Pg 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.
|
||||||
|
|
||||||
|
All popup windows can be closed/canceled by hitting the Esc key
|
||||||
|
(you might need to wait a second for an Esc to register)
|
||||||
|
|
||||||
|
The actions you can perform and the keys to perform them are as follows:
|
||||||
|
|
||||||
|
'h' - Show this help
|
||||||
|
|
||||||
|
'a' - Add a torrent
|
||||||
|
|
||||||
|
'p' - View/Set preferences
|
||||||
|
|
||||||
|
'/' - Search torrent names. Enter to exectue search, ESC to cancel
|
||||||
|
|
||||||
|
'n' - Next matching torrent for last search
|
||||||
|
|
||||||
|
'f' - Show only torrents in a certain state
|
||||||
|
(Will open a popup where you can select the state you want to see)
|
||||||
|
|
||||||
|
'i' - Show more detailed information about the current selected torrent
|
||||||
|
|
||||||
|
'Q' - quit
|
||||||
|
|
||||||
|
'm' - Mark a torrent
|
||||||
|
'M' - Mark all torrents between currently selected torrent
|
||||||
|
and last marked torrent
|
||||||
|
'c' - Un-mark all torrents
|
||||||
|
|
||||||
|
Right Arrow - 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.
|
||||||
|
|
||||||
|
Enter - Show torrent actions popup. Here you can do things like
|
||||||
|
pause/resume, remove, recheck and so one. These actions
|
||||||
|
apply to all currently marked torrents. The currently
|
||||||
|
selected torrent is automatically marked when you press enter.
|
||||||
|
|
||||||
|
'q'/Esc - Close a popup
|
||||||
|
"""
|
||||||
|
HELP_LINES = HELP_STR.split('\n')
|
||||||
|
|
||||||
|
class FILTER:
|
||||||
|
ALL=0
|
||||||
|
ACTIVE=1
|
||||||
|
DOWNLOADING=2
|
||||||
|
SEEDING=3
|
||||||
|
PAUSED=4
|
||||||
|
CHECKING=5
|
||||||
|
ERROR=6
|
||||||
|
QUEUED=7
|
||||||
|
|
||||||
|
class StateUpdater(component.Component):
|
||||||
|
def __init__(self, cb, sf,tcb):
|
||||||
|
component.Component.__init__(self, "AllTorrentsStateUpdater", 1, depend=["SessionProxy"])
|
||||||
|
self._status_cb = cb
|
||||||
|
self._status_fields = sf
|
||||||
|
self.status_dict = {}
|
||||||
|
self._torrent_cb = tcb
|
||||||
|
self._torrent_to_update = None
|
||||||
|
|
||||||
|
def set_torrent_to_update(self, tid, keys):
|
||||||
|
self._torrent_to_update = tid
|
||||||
|
self._torrent_keys = keys
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
component.get("SessionProxy").get_torrents_status(self.status_dict, self._status_fields).addCallback(self._on_torrents_status,False)
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
component.get("SessionProxy").get_torrents_status(self.status_dict, self._status_fields).addCallback(self._on_torrents_status,True)
|
||||||
|
if self._torrent_to_update:
|
||||||
|
component.get("SessionProxy").get_torrent_status(self._torrent_to_update, self._torrent_keys).addCallback(self._torrent_cb)
|
||||||
|
|
||||||
|
def _on_torrents_status(self, state, refresh):
|
||||||
|
self._status_cb(state,refresh)
|
||||||
|
|
||||||
|
|
||||||
|
class AllTorrents(BaseMode):
|
||||||
|
def __init__(self, stdscr, encoding=None):
|
||||||
|
self.formatted_rows = None
|
||||||
|
self.torrent_names = None
|
||||||
|
self.cursel = 1
|
||||||
|
self.curoff = 1 # TODO: this should really be 0 indexed
|
||||||
|
self.column_string = ""
|
||||||
|
self.popup = None
|
||||||
|
self.messages = deque()
|
||||||
|
self.marked = []
|
||||||
|
self.last_mark = -1
|
||||||
|
self._sorted_ids = None
|
||||||
|
self._go_top = False
|
||||||
|
|
||||||
|
self._curr_filter = None
|
||||||
|
self.entering_search = False
|
||||||
|
self.search_string = None
|
||||||
|
self.cursor = 0
|
||||||
|
|
||||||
|
self.coreconfig = component.get("ConsoleUI").coreconfig
|
||||||
|
|
||||||
|
BaseMode.__init__(self, stdscr, encoding)
|
||||||
|
curses.curs_set(0)
|
||||||
|
self.stdscr.notimeout(0)
|
||||||
|
|
||||||
|
self._status_fields = ["queue","name","total_wanted","state","progress","num_seeds","total_seeds",
|
||||||
|
"num_peers","total_peers","download_payload_rate", "upload_payload_rate"]
|
||||||
|
|
||||||
|
self.updater = StateUpdater(self.set_state,self._status_fields,self._on_torrent_status)
|
||||||
|
|
||||||
|
self.column_names = ["#", "Name","Size","State","Progress","Seeders","Peers","Down Speed","Up Speed"]
|
||||||
|
self._update_columns()
|
||||||
|
|
||||||
|
|
||||||
|
self._info_fields = [
|
||||||
|
("Name",None,("name",)),
|
||||||
|
("State", None, ("state",)),
|
||||||
|
("Down Speed", format_utils.format_speed, ("download_payload_rate",)),
|
||||||
|
("Up Speed", format_utils.format_speed, ("upload_payload_rate",)),
|
||||||
|
("Progress", format_utils.format_progress, ("progress",)),
|
||||||
|
("ETA", deluge.common.ftime, ("eta",)),
|
||||||
|
("Path", None, ("save_path",)),
|
||||||
|
("Downloaded",deluge.common.fsize,("all_time_download",)),
|
||||||
|
("Uploaded", deluge.common.fsize,("total_uploaded",)),
|
||||||
|
("Share Ratio", lambda x:x < 0 and "∞" or "%.3f"%x, ("ratio",)),
|
||||||
|
("Seeders",format_utils.format_seeds_peers,("num_seeds","total_seeds")),
|
||||||
|
("Peers",format_utils.format_seeds_peers,("num_peers","total_peers")),
|
||||||
|
("Active Time",deluge.common.ftime,("active_time",)),
|
||||||
|
("Seeding Time",deluge.common.ftime,("seeding_time",)),
|
||||||
|
("Date Added",deluge.common.fdate,("time_added",)),
|
||||||
|
("Availability", lambda x:x < 0 and "∞" or "%.3f"%x, ("distributed_copies",)),
|
||||||
|
("Pieces", format_utils.format_pieces, ("num_pieces","piece_length")),
|
||||||
|
]
|
||||||
|
|
||||||
|
self._status_keys = ["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","save_path"]
|
||||||
|
|
||||||
|
def resume(self):
|
||||||
|
component.start(["AllTorrentsStateUpdater"])
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
|
||||||
|
def _update_columns(self):
|
||||||
|
self.column_widths = [5,-1,15,13,10,10,10,15,15]
|
||||||
|
req = sum(filter(lambda x:x >= 0,self.column_widths))
|
||||||
|
if (req > self.cols): # can't satisfy requests, just spread out evenly
|
||||||
|
cw = int(self.cols/len(self.column_names))
|
||||||
|
for i in range(0,len(self.column_widths)):
|
||||||
|
self.column_widths[i] = cw
|
||||||
|
else:
|
||||||
|
rem = self.cols - req
|
||||||
|
var_cols = len(filter(lambda x: x < 0,self.column_widths))
|
||||||
|
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!}%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 set_state(self, state, refresh):
|
||||||
|
self.curstate = state # cache in case we change sort order
|
||||||
|
newnames = []
|
||||||
|
newrows = []
|
||||||
|
self._sorted_ids = self._sort_torrents(self.curstate)
|
||||||
|
for torrent_id in self._sorted_ids:
|
||||||
|
ts = self.curstate[torrent_id]
|
||||||
|
newnames.append(ts["name"])
|
||||||
|
newrows.append((format_utils.format_row([self._format_queue(ts["queue"]),
|
||||||
|
ts["name"],
|
||||||
|
"%s"%deluge.common.fsize(ts["total_wanted"]),
|
||||||
|
ts["state"],
|
||||||
|
format_utils.format_progress(ts["progress"]),
|
||||||
|
format_utils.format_seeds_peers(ts["num_seeds"],ts["total_seeds"]),
|
||||||
|
format_utils.format_seeds_peers(ts["num_peers"],ts["total_peers"]),
|
||||||
|
format_utils.format_speed(ts["download_payload_rate"]),
|
||||||
|
format_utils.format_speed(ts["upload_payload_rate"])
|
||||||
|
],self.column_widths),ts["state"]))
|
||||||
|
self.numtorrents = len(state)
|
||||||
|
self.formatted_rows = newrows
|
||||||
|
self.torrent_names = newnames
|
||||||
|
if refresh:
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
def get_torrent_name(self, torrent_id):
|
||||||
|
for p,i in enumerate(self._sorted_ids):
|
||||||
|
if torrent_id == i:
|
||||||
|
return self.torrent_names[p]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _scroll_up(self, by):
|
||||||
|
prevoff = self.curoff
|
||||||
|
self.cursel = max(self.cursel - by,1)
|
||||||
|
if ((self.cursel - 1) < self.curoff):
|
||||||
|
self.curoff = max(self.cursel - 1,1)
|
||||||
|
return prevoff != self.curoff
|
||||||
|
|
||||||
|
def _scroll_down(self, by):
|
||||||
|
prevoff = self.curoff
|
||||||
|
self.cursel = min(self.cursel + by,self.numtorrents)
|
||||||
|
if ((self.curoff + self.rows - 5) < self.cursel):
|
||||||
|
self.curoff = self.cursel - self.rows + 5
|
||||||
|
return prevoff != self.curoff
|
||||||
|
|
||||||
|
def current_torrent_id(self):
|
||||||
|
if self._sorted_ids:
|
||||||
|
return self._sorted_ids[self.cursel-1]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _selected_torrent_ids(self):
|
||||||
|
ret = []
|
||||||
|
for i in self.marked:
|
||||||
|
ret.append(self._sorted_ids[i-1])
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def _on_torrent_status(self, state):
|
||||||
|
if (self.popup):
|
||||||
|
self.popup.clear()
|
||||||
|
name = state["name"]
|
||||||
|
off = int((self.cols/4)-(len(name)/2))
|
||||||
|
self.popup.set_title(name)
|
||||||
|
for i,f in enumerate(self._info_fields):
|
||||||
|
if f[1] != None:
|
||||||
|
args = []
|
||||||
|
try:
|
||||||
|
for key in f[2]:
|
||||||
|
args.append(state[key])
|
||||||
|
except:
|
||||||
|
log.debug("Could not get info field: %s",e)
|
||||||
|
continue
|
||||||
|
info = f[1](*args)
|
||||||
|
else:
|
||||||
|
info = state[f[2][0]]
|
||||||
|
|
||||||
|
self.popup.add_line("{!info!}%s: {!input!}%s"%(f[0],info))
|
||||||
|
self.refresh()
|
||||||
|
else:
|
||||||
|
self.updater.set_torrent_to_update(None,None)
|
||||||
|
|
||||||
|
|
||||||
|
def on_resize(self, *args):
|
||||||
|
BaseMode.on_resize_norefresh(self, *args)
|
||||||
|
self._update_columns()
|
||||||
|
if self.popup:
|
||||||
|
self.popup.handle_resize()
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
def _queue_sort(self, 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
|
||||||
|
|
||||||
|
def _sort_torrents(self, state):
|
||||||
|
"sorts by queue #"
|
||||||
|
return sorted(state,cmp=self._queue_sort,key=lambda s:state.get(s)["queue"])
|
||||||
|
|
||||||
|
def _format_queue(self, qnum):
|
||||||
|
if (qnum >= 0):
|
||||||
|
return "%d"%(qnum+1)
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def show_torrent_details(self,tid):
|
||||||
|
component.stop(["AllTorrentsStateUpdater"])
|
||||||
|
self.stdscr.clear()
|
||||||
|
td = TorrentDetail(self,tid,self.stdscr,self.encoding)
|
||||||
|
component.get("ConsoleUI").set_mode(td)
|
||||||
|
|
||||||
|
def show_preferences(self):
|
||||||
|
def _on_get_config(config):
|
||||||
|
client.core.get_listen_port().addCallback(_on_get_listen_port,config)
|
||||||
|
|
||||||
|
def _on_get_listen_port(port,config):
|
||||||
|
client.core.get_cache_status().addCallback(_on_get_cache_status,port,config)
|
||||||
|
|
||||||
|
def _on_get_cache_status(status,port,config):
|
||||||
|
component.stop(["AllTorrentsStateUpdater"])
|
||||||
|
self.stdscr.clear()
|
||||||
|
prefs = Preferences(self,config,port,status,self.stdscr,self.encoding)
|
||||||
|
component.get("ConsoleUI").set_mode(prefs)
|
||||||
|
|
||||||
|
client.core.get_config().addCallback(_on_get_config)
|
||||||
|
|
||||||
|
|
||||||
|
def __show_events(self):
|
||||||
|
component.stop(["AllTorrentsStateUpdater"])
|
||||||
|
self.stdscr.clear()
|
||||||
|
ev = EventView(self,self.stdscr,self.encoding)
|
||||||
|
component.get("ConsoleUI").set_mode(ev)
|
||||||
|
|
||||||
|
def _torrent_filter(self, idx, data):
|
||||||
|
if data==FILTER.ALL:
|
||||||
|
self.updater.status_dict = {}
|
||||||
|
self._curr_filter = None
|
||||||
|
elif data==FILTER.ACTIVE:
|
||||||
|
self.updater.status_dict = {"state":"Active"}
|
||||||
|
self._curr_filter = "Active"
|
||||||
|
elif data==FILTER.DOWNLOADING:
|
||||||
|
self.updater.status_dict = {"state":"Downloading"}
|
||||||
|
self._curr_filter = "Downloading"
|
||||||
|
elif data==FILTER.SEEDING:
|
||||||
|
self.updater.status_dict = {"state":"Seeding"}
|
||||||
|
self._curr_filter = "Seeding"
|
||||||
|
elif data==FILTER.PAUSED:
|
||||||
|
self.updater.status_dict = {"state":"Paused"}
|
||||||
|
self._curr_filter = "Paused"
|
||||||
|
elif data==FILTER.CHECKING:
|
||||||
|
self.updater.status_dict = {"state":"Checking"}
|
||||||
|
self._curr_filter = "Checking"
|
||||||
|
elif data==FILTER.ERROR:
|
||||||
|
self.updater.status_dict = {"state":"Error"}
|
||||||
|
self._curr_filter = "Error"
|
||||||
|
elif data==FILTER.QUEUED:
|
||||||
|
self.updater.status_dict = {"state":"Queued"}
|
||||||
|
self._curr_filter = "Queued"
|
||||||
|
self._go_top = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _show_torrent_filter_popup(self):
|
||||||
|
self.popup = SelectablePopup(self,"Filter Torrents",self._torrent_filter)
|
||||||
|
self.popup.add_line("_All",data=FILTER.ALL)
|
||||||
|
self.popup.add_line("Ac_tive",data=FILTER.ACTIVE)
|
||||||
|
self.popup.add_line("_Downloading",data=FILTER.DOWNLOADING,foreground="green")
|
||||||
|
self.popup.add_line("_Seeding",data=FILTER.SEEDING,foreground="cyan")
|
||||||
|
self.popup.add_line("_Paused",data=FILTER.PAUSED)
|
||||||
|
self.popup.add_line("_Error",data=FILTER.ERROR,foreground="red")
|
||||||
|
self.popup.add_line("_Checking",data=FILTER.CHECKING,foreground="blue")
|
||||||
|
self.popup.add_line("Q_ueued",data=FILTER.QUEUED,foreground="yellow")
|
||||||
|
|
||||||
|
def __report_add_status(self, succ_cnt, fail_cnt, fail_msgs):
|
||||||
|
if fail_cnt == 0:
|
||||||
|
self.report_message("Torrents Added","{!success!}Sucessfully added %d torrent(s)"%succ_cnt)
|
||||||
|
else:
|
||||||
|
msg = ("{!error!}Failed to add the following %d torrent(s):\n {!error!}"%fail_cnt)+"\n {!error!}".join(fail_msgs)
|
||||||
|
if succ_cnt != 0:
|
||||||
|
msg += "\n \n{!success!}Sucessfully added %d torrent(s)"%succ_cnt
|
||||||
|
self.report_message("Torrent Add Report",msg)
|
||||||
|
|
||||||
|
def _do_add(self, result):
|
||||||
|
log.debug("Adding Torrent(s): %s (dl path: %s) (paused: %d)",result["file"],result["path"],result["add_paused"])
|
||||||
|
ress = {"succ":0,
|
||||||
|
"fail":0,
|
||||||
|
"fmsg":[]}
|
||||||
|
|
||||||
|
def fail_cb(msg,t_file,ress):
|
||||||
|
log.debug("failed to add torrent: %s: %s"%(t_file,msg))
|
||||||
|
ress["fail"]+=1
|
||||||
|
ress["fmsg"].append("%s: %s"%(t_file,msg))
|
||||||
|
if (ress["succ"]+ress["fail"]) >= ress["total"]:
|
||||||
|
self.__report_add_status(ress["succ"],ress["fail"],ress["fmsg"])
|
||||||
|
def suc_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.__report_add_status(ress["succ"],ress["fail"],ress["fmsg"])
|
||||||
|
else:
|
||||||
|
fail_cb("Already in session (probably)",t_file,ress)
|
||||||
|
|
||||||
|
add_torrent(result["file"],result,suc_cb,fail_cb,ress)
|
||||||
|
|
||||||
|
def _show_torrent_add_popup(self):
|
||||||
|
dl = ""
|
||||||
|
ap = 1
|
||||||
|
try:
|
||||||
|
dl = self.coreconfig["download_location"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
if self.coreconfig["add_paused"]:
|
||||||
|
ap = 0
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.popup = InputPopup(self,"Add Torrent (Esc to cancel)",close_cb=self._do_add)
|
||||||
|
self.popup.add_text_input("Enter path to torrent file:","file")
|
||||||
|
self.popup.add_text_input("Enter save path:","path",dl)
|
||||||
|
self.popup.add_select_input("Add Paused:","add_paused",["Yes","No"],[True,False],ap)
|
||||||
|
|
||||||
|
def report_message(self,title,message):
|
||||||
|
self.messages.append((title,message))
|
||||||
|
|
||||||
|
def clear_marks(self):
|
||||||
|
self.marked = []
|
||||||
|
self.last_mark = -1
|
||||||
|
|
||||||
|
def set_popup(self,pu):
|
||||||
|
self.popup = pu
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
def refresh(self,lines=None):
|
||||||
|
#log.error("ref")
|
||||||
|
#import traceback
|
||||||
|
#traceback.print_stack()
|
||||||
|
# Something has requested we scroll to the top of the list
|
||||||
|
if self._go_top:
|
||||||
|
self.cursel = 1
|
||||||
|
self.curoff = 1
|
||||||
|
self._go_top = False
|
||||||
|
|
||||||
|
# show a message popup if there's anything queued
|
||||||
|
if self.popup == None and self.messages:
|
||||||
|
title,msg = self.messages.popleft()
|
||||||
|
self.popup = MessagePopup(self,title,msg)
|
||||||
|
|
||||||
|
if not lines:
|
||||||
|
self.stdscr.clear()
|
||||||
|
|
||||||
|
# Update the status bars
|
||||||
|
if self._curr_filter == None:
|
||||||
|
self.add_string(0,self.statusbars.topbar)
|
||||||
|
else:
|
||||||
|
self.add_string(0,"%s {!filterstatus!}Current filter: %s"%(self.statusbars.topbar,self._curr_filter))
|
||||||
|
self.add_string(1,self.column_string)
|
||||||
|
|
||||||
|
if self.entering_search:
|
||||||
|
self.add_string(self.rows - 1,"{!black,white!}Search torrents: %s"%self.search_string)
|
||||||
|
else:
|
||||||
|
hstr = "%sPress [h] for help"%(" "*(self.cols - len(self.statusbars.bottombar) - 10))
|
||||||
|
self.add_string(self.rows - 1, "%s%s"%(self.statusbars.bottombar,hstr))
|
||||||
|
|
||||||
|
# add all the torrents
|
||||||
|
if self.formatted_rows == []:
|
||||||
|
msg = "No torrents match filter".center(self.cols)
|
||||||
|
self.add_string(3, "{!info!}%s"%msg)
|
||||||
|
elif self.formatted_rows:
|
||||||
|
tidx = self.curoff
|
||||||
|
currow = 2
|
||||||
|
|
||||||
|
if lines:
|
||||||
|
todraw = []
|
||||||
|
for l in lines:
|
||||||
|
todraw.append(self.formatted_rows[l])
|
||||||
|
lines.reverse()
|
||||||
|
else:
|
||||||
|
todraw = self.formatted_rows[tidx-1:]
|
||||||
|
|
||||||
|
for row in todraw:
|
||||||
|
# default style
|
||||||
|
fg = "white"
|
||||||
|
bg = "black"
|
||||||
|
attr = None
|
||||||
|
if lines:
|
||||||
|
tidx = lines.pop()+1
|
||||||
|
currow = tidx-self.curoff+2
|
||||||
|
|
||||||
|
if tidx in self.marked:
|
||||||
|
bg = "blue"
|
||||||
|
attr = "bold"
|
||||||
|
|
||||||
|
if tidx == self.cursel:
|
||||||
|
bg = "white"
|
||||||
|
attr = "bold"
|
||||||
|
if tidx in self.marked:
|
||||||
|
fg = "blue"
|
||||||
|
else:
|
||||||
|
fg = "black"
|
||||||
|
|
||||||
|
if row[1] == "Downloading":
|
||||||
|
fg = "green"
|
||||||
|
elif row[1] == "Seeding":
|
||||||
|
fg = "cyan"
|
||||||
|
elif row[1] == "Error":
|
||||||
|
fg = "red"
|
||||||
|
elif row[1] == "Queued":
|
||||||
|
fg = "yellow"
|
||||||
|
elif row[1] == "Checking":
|
||||||
|
fg = "blue"
|
||||||
|
|
||||||
|
if attr:
|
||||||
|
colorstr = "{!%s,%s,%s!}"%(fg,bg,attr)
|
||||||
|
else:
|
||||||
|
colorstr = "{!%s,%s!}"%(fg,bg)
|
||||||
|
|
||||||
|
self.add_string(currow,"%s%s"%(colorstr,row[0]))
|
||||||
|
tidx += 1
|
||||||
|
currow += 1
|
||||||
|
if (currow > (self.rows - 2)):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.add_string(1, "Waiting for torrents from core...")
|
||||||
|
|
||||||
|
#self.stdscr.redrawwin()
|
||||||
|
if self.entering_search:
|
||||||
|
curses.curs_set(2)
|
||||||
|
self.stdscr.move(self.rows-1,self.cursor+17)
|
||||||
|
else:
|
||||||
|
curses.curs_set(0)
|
||||||
|
|
||||||
|
self.stdscr.noutrefresh()
|
||||||
|
|
||||||
|
if self.popup:
|
||||||
|
self.popup.refresh()
|
||||||
|
|
||||||
|
curses.doupdate()
|
||||||
|
|
||||||
|
|
||||||
|
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 __do_search(self):
|
||||||
|
# search forward for the next torrent matching self.search_string
|
||||||
|
for i,n in enumerate(self.torrent_names[self.cursel:]):
|
||||||
|
if n.find(self.search_string) >= 0:
|
||||||
|
self.cursel += (i+1)
|
||||||
|
if ((self.curoff + self.rows - 5) < self.cursel):
|
||||||
|
self.curoff = self.cursel - self.rows + 5
|
||||||
|
return
|
||||||
|
|
||||||
|
def __update_search(self, c):
|
||||||
|
if c == curses.KEY_BACKSPACE or c == 127:
|
||||||
|
if self.search_string and self.cursor > 0:
|
||||||
|
self.search_string = self.search_string[:self.cursor - 1] + self.search_string[self.cursor:]
|
||||||
|
self.cursor-=1
|
||||||
|
elif c == curses.KEY_DC:
|
||||||
|
if self.search_string and self.cursor < len(self.search_string):
|
||||||
|
self.search_string = self.search_string[:self.cursor] + self.search_string[self.cursor+1:]
|
||||||
|
elif c == curses.KEY_LEFT:
|
||||||
|
self.cursor = max(0,self.cursor-1)
|
||||||
|
elif c == curses.KEY_RIGHT:
|
||||||
|
self.cursor = min(len(self.search_string),self.cursor+1)
|
||||||
|
elif c == curses.KEY_HOME:
|
||||||
|
self.cursor = 0
|
||||||
|
elif c == curses.KEY_END:
|
||||||
|
self.cursor = len(self.search_string)
|
||||||
|
elif c == 27:
|
||||||
|
self.search_string = None
|
||||||
|
self.entering_search = False
|
||||||
|
elif c == 10 or c == curses.KEY_ENTER:
|
||||||
|
self.entering_search = False
|
||||||
|
self.__do_search()
|
||||||
|
elif c > 31 and c < 256:
|
||||||
|
stroke = chr(c)
|
||||||
|
uchar = ""
|
||||||
|
while not uchar:
|
||||||
|
try:
|
||||||
|
uchar = stroke.decode(self.encoding)
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
c = self.stdscr.getch()
|
||||||
|
stroke += chr(c)
|
||||||
|
if uchar:
|
||||||
|
if self.cursor == len(self.search_string):
|
||||||
|
self.search_string += uchar
|
||||||
|
else:
|
||||||
|
# Insert into string
|
||||||
|
self.search_string = self.search_string[:self.cursor] + uchar + self.search_string[self.cursor:]
|
||||||
|
# Move the cursor forward
|
||||||
|
self.cursor+=1
|
||||||
|
|
||||||
|
def _doRead(self):
|
||||||
|
# Read the character
|
||||||
|
effected_lines = None
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if self.formatted_rows==None or self.popup:
|
||||||
|
return
|
||||||
|
|
||||||
|
elif self.entering_search:
|
||||||
|
self.__update_search(c)
|
||||||
|
self.refresh([])
|
||||||
|
return
|
||||||
|
|
||||||
|
#log.error("pressed key: %d\n",c)
|
||||||
|
#if c == 27: # handle escape
|
||||||
|
# log.error("CANCEL")
|
||||||
|
|
||||||
|
# Navigate the torrent list
|
||||||
|
if c == curses.KEY_UP:
|
||||||
|
if self.cursel == 1: return
|
||||||
|
if not self._scroll_up(1):
|
||||||
|
effected_lines = [self.cursel-1,self.cursel]
|
||||||
|
elif c == curses.KEY_PPAGE:
|
||||||
|
self._scroll_up(int(self.rows/2))
|
||||||
|
elif c == curses.KEY_DOWN:
|
||||||
|
if self.cursel >= self.numtorrents: return
|
||||||
|
if not self._scroll_down(1):
|
||||||
|
effected_lines = [self.cursel-2,self.cursel-1]
|
||||||
|
elif c == curses.KEY_NPAGE:
|
||||||
|
self._scroll_down(int(self.rows/2))
|
||||||
|
elif c == curses.KEY_HOME:
|
||||||
|
self._scroll_up(self.cursel)
|
||||||
|
elif c == curses.KEY_END:
|
||||||
|
self._scroll_down(self.numtorrents-self.cursel)
|
||||||
|
|
||||||
|
elif c == curses.KEY_RIGHT:
|
||||||
|
# We enter a new mode for the selected torrent here
|
||||||
|
tid = self.current_torrent_id()
|
||||||
|
if tid:
|
||||||
|
self.show_torrent_details(tid)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Enter Key
|
||||||
|
elif (c == curses.KEY_ENTER or c == 10) and self.numtorrents:
|
||||||
|
self.marked.append(self.cursel)
|
||||||
|
self.last_mark = self.cursel
|
||||||
|
torrent_actions_popup(self,self._selected_torrent_ids(),details=True)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
if c > 31 and c < 256:
|
||||||
|
if chr(c) == '/':
|
||||||
|
self.search_string = ""
|
||||||
|
self.cursor = 0
|
||||||
|
self.entering_search = True
|
||||||
|
elif chr(c) == 'n' and self.search_string:
|
||||||
|
self.__do_search()
|
||||||
|
elif chr(c) == 'j':
|
||||||
|
if not self._scroll_up(1):
|
||||||
|
effected_lines = [self.cursel-1,self.cursel]
|
||||||
|
elif chr(c) == 'k':
|
||||||
|
if not self._scroll_down(1):
|
||||||
|
effected_lines = [self.cursel-2,self.cursel-1]
|
||||||
|
elif chr(c) == 'i':
|
||||||
|
cid = self.current_torrent_id()
|
||||||
|
if cid:
|
||||||
|
self.popup = Popup(self,"Info",close_cb=lambda:self.updater.set_torrent_to_update(None,None))
|
||||||
|
self.popup.add_line("Getting torrent info...")
|
||||||
|
self.updater.set_torrent_to_update(cid,self._status_keys)
|
||||||
|
elif chr(c) == 'm':
|
||||||
|
self._mark_unmark(self.cursel)
|
||||||
|
effected_lines = [self.cursel-1]
|
||||||
|
elif chr(c) == 'M':
|
||||||
|
if self.last_mark >= 0:
|
||||||
|
self.marked.extend(range(self.last_mark,self.cursel+1))
|
||||||
|
effected_lines = range(self.last_mark,self.cursel)
|
||||||
|
else:
|
||||||
|
self._mark_unmark(self.cursel)
|
||||||
|
effected_lines = [self.cursel-1]
|
||||||
|
elif chr(c) == 'c':
|
||||||
|
self.marked = []
|
||||||
|
self.last_mark = -1
|
||||||
|
elif chr(c) == 'a':
|
||||||
|
self._show_torrent_add_popup()
|
||||||
|
elif chr(c) == 'f':
|
||||||
|
self._show_torrent_filter_popup()
|
||||||
|
elif chr(c) == 'h':
|
||||||
|
self.popup = Popup(self,"Help")
|
||||||
|
for l in HELP_LINES:
|
||||||
|
self.popup.add_line(l)
|
||||||
|
elif chr(c) == 'p':
|
||||||
|
self.show_preferences()
|
||||||
|
return
|
||||||
|
elif chr(c) == 'e':
|
||||||
|
self.__show_events()
|
||||||
|
return
|
||||||
|
|
||||||
|
self.refresh(effected_lines)
|
|
@ -0,0 +1,240 @@
|
||||||
|
#
|
||||||
|
# basemode.py
|
||||||
|
#
|
||||||
|
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||||
|
#
|
||||||
|
# Most code in this file taken from screen.py:
|
||||||
|
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||||
|
#
|
||||||
|
# Deluge is free software.
|
||||||
|
#
|
||||||
|
# You may redistribute it and/or modify it under the terms of the
|
||||||
|
# GNU General Public License, as published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option)
|
||||||
|
# any later version.
|
||||||
|
#
|
||||||
|
# deluge is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
# See the GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with deluge. If not, write to:
|
||||||
|
# The Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin Street, Fifth Floor
|
||||||
|
# Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# In addition, as a special exception, the copyright holders give
|
||||||
|
# permission to link the code of portions of this program with the OpenSSL
|
||||||
|
# library.
|
||||||
|
# You must obey the GNU General Public License in all respects for all of
|
||||||
|
# the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
# exception, you may extend this exception to your version of the file(s),
|
||||||
|
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||||
|
# this exception statement from your version. If you delete this exception
|
||||||
|
# statement from all source files in the program, then also delete it here.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
try:
|
||||||
|
import curses
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
import deluge.component as component
|
||||||
|
import deluge.ui.console.colors as colors
|
||||||
|
try:
|
||||||
|
import signal
|
||||||
|
from fcntl import ioctl
|
||||||
|
import termios
|
||||||
|
import struct
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
from twisted.internet import reactor
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class CursesStdIO(object):
|
||||||
|
"""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 """
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def doRead(self):
|
||||||
|
"""called when input is ready"""
|
||||||
|
pass
|
||||||
|
def logPrefix(self): return 'CursesClient'
|
||||||
|
|
||||||
|
|
||||||
|
class BaseMode(CursesStdIO):
|
||||||
|
def __init__(self, stdscr, encoding=None, do_refresh=True):
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
Modes should subclass this and provide overrides for:
|
||||||
|
|
||||||
|
_doRead(self) - Handle user input
|
||||||
|
refresh(self) - draw the mode to the screen
|
||||||
|
add_string(self, row, string) - add a string of text to be displayed.
|
||||||
|
see method for detailed info
|
||||||
|
|
||||||
|
The init method of a subclass *must* call BaseMode.__init__
|
||||||
|
|
||||||
|
Useful fields after calling BaseMode.__init__:
|
||||||
|
self.stdscr - the curses screen
|
||||||
|
self.rows - # of rows on the curses screen
|
||||||
|
self.cols - # of cols on the curses screen
|
||||||
|
self.topbar - top statusbar
|
||||||
|
self.bottombar - bottom statusbar
|
||||||
|
"""
|
||||||
|
log.debug("BaseMode init!")
|
||||||
|
self.stdscr = stdscr
|
||||||
|
# Make the input calls non-blocking
|
||||||
|
self.stdscr.nodelay(1)
|
||||||
|
|
||||||
|
# Strings for the 2 status bars
|
||||||
|
self.statusbars = component.get("StatusBars")
|
||||||
|
|
||||||
|
# Keep track of the screen size
|
||||||
|
self.rows, self.cols = self.stdscr.getmaxyx()
|
||||||
|
try:
|
||||||
|
signal.signal(signal.SIGWINCH, self.on_resize)
|
||||||
|
except Exception, e:
|
||||||
|
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 connectionLost(self, reason):
|
||||||
|
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!"
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
if scr:
|
||||||
|
screen = scr
|
||||||
|
else:
|
||||||
|
screen = self.stdscr
|
||||||
|
try:
|
||||||
|
parsed = colors.parse_color_string(string, self.encoding)
|
||||||
|
except colors.BadColorString, e:
|
||||||
|
log.error("Cannot add bad color string %s: %s", string, e)
|
||||||
|
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:
|
||||||
|
y,x = screen.getmaxyx()
|
||||||
|
if (col+len(s)) > x:
|
||||||
|
s = "%s..."%s[0:x-4-col]
|
||||||
|
screen.addstr(row, col, s, color)
|
||||||
|
col += len(s)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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 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.
|
||||||
|
"""
|
||||||
|
self.stdscr.clear()
|
||||||
|
self.draw_statusbars()
|
||||||
|
# Update the status bars
|
||||||
|
|
||||||
|
self.add_string(1,"{!info!}Base Mode (or subclass hasn't overridden refresh)")
|
||||||
|
|
||||||
|
self.stdscr.redrawwin()
|
||||||
|
self.stdscr.refresh()
|
||||||
|
|
||||||
|
def doRead(self):
|
||||||
|
"""
|
||||||
|
Called when there is data to be read, ie, input from the keyboard.
|
||||||
|
"""
|
||||||
|
# We wrap this function to catch exceptions and shutdown the mainloop
|
||||||
|
try:
|
||||||
|
self._doRead()
|
||||||
|
except Exception, e:
|
||||||
|
log.exception(e)
|
||||||
|
reactor.stop()
|
||||||
|
|
||||||
|
def _doRead(self):
|
||||||
|
# Read the character
|
||||||
|
c = self.stdscr.getch()
|
||||||
|
self.stdscr.refresh()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""
|
||||||
|
Clean up the curses stuff on exit.
|
||||||
|
"""
|
||||||
|
curses.nocbreak()
|
||||||
|
self.stdscr.keypad(0)
|
||||||
|
curses.echo()
|
||||||
|
curses.endwin()
|
|
@ -0,0 +1,219 @@
|
||||||
|
#
|
||||||
|
# connectionmanager.py
|
||||||
|
#
|
||||||
|
# Copyright (C) 2007-2009 Nick Lanham <nick@afternight.org>
|
||||||
|
#
|
||||||
|
# Deluge is free software.
|
||||||
|
#
|
||||||
|
# You may redistribute it and/or modify it under the terms of the
|
||||||
|
# GNU General Public License, as published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option)
|
||||||
|
# any later version.
|
||||||
|
#
|
||||||
|
# deluge is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
# See the GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with deluge. If not, write to:
|
||||||
|
# The Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin Street, Fifth Floor
|
||||||
|
# Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# In addition, as a special exception, the copyright holders give
|
||||||
|
# permission to link the code of portions of this program with the OpenSSL
|
||||||
|
# library.
|
||||||
|
# You must obey the GNU General Public License in all respects for all of
|
||||||
|
# the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
# exception, you may extend this exception to your version of the file(s),
|
||||||
|
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||||
|
# this exception statement from your version. If you delete this exception
|
||||||
|
# statement from all source files in the program, then also delete it here.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
# a mode that show's a popup to select which host to connect to
|
||||||
|
|
||||||
|
import hashlib,time
|
||||||
|
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
import deluge.ui.client
|
||||||
|
from deluge.ui.client import client
|
||||||
|
from deluge.configmanager import ConfigManager
|
||||||
|
from deluge.ui.coreconfig import CoreConfig
|
||||||
|
import deluge.component as component
|
||||||
|
|
||||||
|
from alltorrents import AllTorrents
|
||||||
|
from basemode import BaseMode
|
||||||
|
from popup import SelectablePopup,MessagePopup
|
||||||
|
from input_popup import InputPopup
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
import curses
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
import logging
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEFAULT_HOST = "127.0.0.1"
|
||||||
|
DEFAULT_PORT = 58846
|
||||||
|
|
||||||
|
DEFAULT_CONFIG = {
|
||||||
|
"hosts": [(hashlib.sha1(str(time.time())).hexdigest(), DEFAULT_HOST, DEFAULT_PORT, "", "")]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionManager(BaseMode):
|
||||||
|
def __init__(self, stdscr, encoding=None):
|
||||||
|
self.popup = None
|
||||||
|
self.statuses = {}
|
||||||
|
self.messages = deque()
|
||||||
|
self.config = ConfigManager("hostlist.conf.1.2", DEFAULT_CONFIG)
|
||||||
|
BaseMode.__init__(self, stdscr, encoding)
|
||||||
|
self.__update_statuses()
|
||||||
|
self.__update_popup()
|
||||||
|
|
||||||
|
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"]:
|
||||||
|
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")
|
||||||
|
self.inlist = True
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
def __update_statuses(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()
|
||||||
|
c.disconnect()
|
||||||
|
|
||||||
|
def on_info_fail(reason, c):
|
||||||
|
if host_id in self.statuses:
|
||||||
|
del self.statuses[host_id]
|
||||||
|
c.disconnect()
|
||||||
|
|
||||||
|
d = c.daemon.info()
|
||||||
|
d.addCallback(on_info, c)
|
||||||
|
d.addErrback(on_info_fail, c)
|
||||||
|
|
||||||
|
def on_connect_failed(reason, host_id):
|
||||||
|
if host_id in self.statuses:
|
||||||
|
del self.statuses[host_id]
|
||||||
|
|
||||||
|
for host in self.config["hosts"]:
|
||||||
|
c = deluge.ui.client.Client()
|
||||||
|
hadr = host[1]
|
||||||
|
port = host[2]
|
||||||
|
user = host[3]
|
||||||
|
password = host[4]
|
||||||
|
d = c.connect(hadr, port, user, password)
|
||||||
|
d.addCallback(on_connect, c, host[0])
|
||||||
|
d.addErrback(on_connect_failed, host[0])
|
||||||
|
|
||||||
|
def __on_connected(self,result):
|
||||||
|
component.start()
|
||||||
|
self.stdscr.clear()
|
||||||
|
at = AllTorrents(self.stdscr, self.encoding)
|
||||||
|
component.get("ConsoleUI").set_mode(at)
|
||||||
|
at.resume()
|
||||||
|
|
||||||
|
def __host_selected(self, idx, data):
|
||||||
|
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)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __do_add(self,result):
|
||||||
|
hostname = result["hostname"]
|
||||||
|
try:
|
||||||
|
port = int(result["port"])
|
||||||
|
except ValueError:
|
||||||
|
self.report_message("Can't add host","Invalid port. Must be an integer")
|
||||||
|
return
|
||||||
|
username = result["username"]
|
||||||
|
password = result["password"]
|
||||||
|
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")
|
||||||
|
return
|
||||||
|
newid = hashlib.sha1(str(time.time())).hexdigest()
|
||||||
|
self.config["hosts"].append((newid, hostname, port, username, password))
|
||||||
|
self.config.save()
|
||||||
|
self.__update_popup()
|
||||||
|
|
||||||
|
def __add_popup(self):
|
||||||
|
self.inlist = False
|
||||||
|
self.popup = InputPopup(self,"Add Host (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")
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
def __delete_current_host(self):
|
||||||
|
idx,data = self.popup.current_selection()
|
||||||
|
log.debug("deleting host: %s",data)
|
||||||
|
for host in self.config["hosts"]:
|
||||||
|
if host[0] == data:
|
||||||
|
self.config["hosts"].remove(host)
|
||||||
|
break
|
||||||
|
self.config.save()
|
||||||
|
|
||||||
|
def report_message(self,title,message):
|
||||||
|
self.messages.append((title,message))
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
self.stdscr.clear()
|
||||||
|
self.draw_statusbars()
|
||||||
|
self.stdscr.noutrefresh()
|
||||||
|
|
||||||
|
if self.popup == None and self.messages:
|
||||||
|
title,msg = self.messages.popleft()
|
||||||
|
self.popup = MessagePopup(self,title,msg)
|
||||||
|
|
||||||
|
if not self.popup:
|
||||||
|
self.__update_popup()
|
||||||
|
|
||||||
|
self.popup.refresh()
|
||||||
|
curses.doupdate()
|
||||||
|
|
||||||
|
|
||||||
|
def _doRead(self):
|
||||||
|
# Read the character
|
||||||
|
c = self.stdscr.getch()
|
||||||
|
|
||||||
|
if c > 31 and c < 256:
|
||||||
|
if chr(c) == 'q' and self.inlist: return
|
||||||
|
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
|
||||||
|
if chr(c) == 'D' and self.inlist:
|
||||||
|
self.__delete_current_host()
|
||||||
|
self.__update_popup()
|
||||||
|
return
|
||||||
|
if chr(c) == 'r' and self.inlist:
|
||||||
|
self.__update_statuses()
|
||||||
|
if chr(c) == 'a' and self.inlist:
|
||||||
|
self.__add_popup()
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.popup:
|
||||||
|
if self.popup.handle_read(c):
|
||||||
|
self.popup = None
|
||||||
|
self.refresh()
|
||||||
|
return
|
|
@ -0,0 +1,105 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# eventview.py
|
||||||
|
#
|
||||||
|
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||||
|
#
|
||||||
|
# Deluge is free software.
|
||||||
|
#
|
||||||
|
# You may redistribute it and/or modify it under the terms of the
|
||||||
|
# GNU General Public License, as published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option)
|
||||||
|
# any later version.
|
||||||
|
#
|
||||||
|
# deluge is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
# See the GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with deluge. If not, write to:
|
||||||
|
# The Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin Street, Fifth Floor
|
||||||
|
# Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# In addition, as a special exception, the copyright holders give
|
||||||
|
# permission to link the code of portions of this program with the OpenSSL
|
||||||
|
# library.
|
||||||
|
# You must obey the GNU General Public License in all respects for all of
|
||||||
|
# the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
# exception, you may extend this exception to your version of the file(s),
|
||||||
|
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||||
|
# this exception statement from your version. If you delete this exception
|
||||||
|
# statement from all source files in the program, then also delete it here.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
import deluge.component as component
|
||||||
|
from basemode import BaseMode
|
||||||
|
try:
|
||||||
|
import curses
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
import logging
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class EventView(BaseMode):
|
||||||
|
def __init__(self, parent_mode, stdscr, encoding=None):
|
||||||
|
self.parent_mode = parent_mode
|
||||||
|
BaseMode.__init__(self, stdscr, encoding)
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
"This method just shows each line of the event log"
|
||||||
|
events = component.get("ConsoleUI").events
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
if events:
|
||||||
|
for i,event in enumerate(events):
|
||||||
|
self.add_string(i+1,event)
|
||||||
|
else:
|
||||||
|
self.add_string(1,"{!white,black,bold!}No events to show yet")
|
||||||
|
|
||||||
|
self.stdscr.noutrefresh()
|
||||||
|
curses.doupdate()
|
||||||
|
|
||||||
|
def back_to_overview(self):
|
||||||
|
self.stdscr.clear()
|
||||||
|
component.get("ConsoleUI").set_mode(self.parent_mode)
|
||||||
|
self.parent_mode.resume()
|
||||||
|
|
||||||
|
def _doRead(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:
|
||||||
|
self.back_to_overview()
|
||||||
|
return
|
||||||
|
|
||||||
|
# TODO: Scroll event list
|
||||||
|
if c == curses.KEY_UP:
|
||||||
|
pass
|
||||||
|
elif c == curses.KEY_PPAGE:
|
||||||
|
pass
|
||||||
|
elif c == curses.KEY_DOWN:
|
||||||
|
pass
|
||||||
|
elif c == curses.KEY_NPAGE:
|
||||||
|
pass
|
||||||
|
|
||||||
|
#self.refresh()
|
|
@ -0,0 +1,71 @@
|
||||||
|
# format_utils.py
|
||||||
|
#
|
||||||
|
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||||
|
#
|
||||||
|
# Deluge is free software.
|
||||||
|
#
|
||||||
|
# You may redistribute it and/or modify it under the terms of the
|
||||||
|
# GNU General Public License, as published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option)
|
||||||
|
# any later version.
|
||||||
|
#
|
||||||
|
# deluge is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
# See the GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with deluge. If not, write to:
|
||||||
|
# The Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin Street, Fifth Floor
|
||||||
|
# Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# In addition, as a special exception, the copyright holders give
|
||||||
|
# permission to link the code of portions of this program with the OpenSSL
|
||||||
|
# library.
|
||||||
|
# You must obey the GNU General Public License in all respects for all of
|
||||||
|
# the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
# exception, you may extend this exception to your version of the file(s),
|
||||||
|
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||||
|
# this exception statement from your version. If you delete this exception
|
||||||
|
# statement from all source files in the program, then also delete it here.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
import deluge.common
|
||||||
|
|
||||||
|
def format_speed(speed):
|
||||||
|
if (speed > 0):
|
||||||
|
return deluge.common.fspeed(speed)
|
||||||
|
else:
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
def format_seeds_peers(num, total):
|
||||||
|
return "%d (%d)"%(num,total)
|
||||||
|
|
||||||
|
def format_progress(perc):
|
||||||
|
return "%.2f%%"%perc
|
||||||
|
|
||||||
|
def format_pieces(num, size):
|
||||||
|
return "%d (%s)"%(num,deluge.common.fsize(size))
|
||||||
|
|
||||||
|
def format_priority(prio):
|
||||||
|
if prio < 0: return "-"
|
||||||
|
pstring = deluge.common.FILE_PRIORITY[prio]
|
||||||
|
if prio > 0:
|
||||||
|
return pstring[:pstring.index("Priority")-1]
|
||||||
|
else:
|
||||||
|
return pstring
|
||||||
|
|
||||||
|
def trim_string(string, w):
|
||||||
|
return "%s... "%(string[0:w-4])
|
||||||
|
|
||||||
|
def format_column(col, lim):
|
||||||
|
size = len(col)
|
||||||
|
if (size >= lim - 1):
|
||||||
|
return trim_string(col,lim)
|
||||||
|
else:
|
||||||
|
return "%s%s"%(col," "*(lim-size))
|
||||||
|
|
||||||
|
def format_row(row,column_widths):
|
||||||
|
return "".join([format_column(row[i],column_widths[i]) for i in range(0,len(row))])
|
|
@ -0,0 +1,616 @@
|
||||||
|
#
|
||||||
|
# input_popup.py
|
||||||
|
#
|
||||||
|
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||||
|
#
|
||||||
|
# Complete function from commands/add.py:
|
||||||
|
# Copyright (C) 2008-2009 Ido Abramovich <ido.deluge@gmail.com>
|
||||||
|
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||||
|
#
|
||||||
|
# Deluge is free software.
|
||||||
|
#
|
||||||
|
# You may redistribute it and/or modify it under the terms of the
|
||||||
|
# GNU General Public License, as published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option)
|
||||||
|
# any later version.
|
||||||
|
#
|
||||||
|
# deluge is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
# See the GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with deluge. If not, write to:
|
||||||
|
# The Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin Street, Fifth Floor
|
||||||
|
# Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# In addition, as a special exception, the copyright holders give
|
||||||
|
# permission to link the code of portions of this program with the OpenSSL
|
||||||
|
# library.
|
||||||
|
# You must obey the GNU General Public License in all respects for all of
|
||||||
|
# the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
# exception, you may extend this exception to your version of the file(s),
|
||||||
|
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||||
|
# this exception statement from your version. If you delete this exception
|
||||||
|
# statement from all source files in the program, then also delete it here.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
try:
|
||||||
|
import curses
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
import logging,os.path
|
||||||
|
|
||||||
|
from popup import Popup
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class InputField:
|
||||||
|
depend = None
|
||||||
|
# render the input. return number of rows taken up
|
||||||
|
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):
|
||||||
|
self.parent = parent
|
||||||
|
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 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):
|
||||||
|
self.parent = parent
|
||||||
|
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 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, max_val):
|
||||||
|
self.parent = parent
|
||||||
|
self.message = message
|
||||||
|
self.name = name
|
||||||
|
self.value = int(value)
|
||||||
|
self.initvalue = self.value
|
||||||
|
self.valstr = "%d"%self.value
|
||||||
|
self.cursor = len(self.valstr)
|
||||||
|
self.cursoff = len(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
|
||||||
|
|
||||||
|
def render(self, screen, row, width, active, col=1, cursor_offset=0):
|
||||||
|
if not active and not self.valstr:
|
||||||
|
self.value = self.initvalue
|
||||||
|
self.valstr = "%d"%self.value
|
||||||
|
self.cursor = len(self.valstr)
|
||||||
|
if not self.valstr:
|
||||||
|
self.parent.add_string(row,"%s [ ]"%self.message,screen,col,False,True)
|
||||||
|
elif active:
|
||||||
|
self.parent.add_string(row,"%s [ {!black,white,bold!}%d{!white,black!} ]"%(self.message,self.value),screen,col,False,True)
|
||||||
|
else:
|
||||||
|
self.parent.add_string(row,"%s [ %d ]"%(self.message,self.value),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:
|
||||||
|
self.value+=1
|
||||||
|
elif c == curses.KEY_NPAGE:
|
||||||
|
self.value-=1
|
||||||
|
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.value)
|
||||||
|
elif c == curses.KEY_BACKSPACE or c == 127:
|
||||||
|
if self.valstr and self.cursor > 0:
|
||||||
|
self.valstr = self.valstr[:self.cursor - 1] + self.valstr[self.cursor:]
|
||||||
|
self.cursor-=1
|
||||||
|
if self.valstr:
|
||||||
|
self.value = int(self.valstr)
|
||||||
|
elif c == curses.KEY_DC:
|
||||||
|
if self.valstr and self.cursor < len(self.valstr):
|
||||||
|
self.valstr = self.valstr[:self.cursor] + self.valstr[self.cursor+1:]
|
||||||
|
elif c > 47 and c < 58:
|
||||||
|
if c == 48 and self.cursor == 0: return
|
||||||
|
if self.cursor == len(self.valstr):
|
||||||
|
self.valstr += chr(c)
|
||||||
|
self.value = int(self.valstr)
|
||||||
|
else:
|
||||||
|
# Insert into string
|
||||||
|
self.valstr = self.valstr[:self.cursor] + chr(c) + self.valstr[self.cursor:]
|
||||||
|
self.value = int(self.valstr)
|
||||||
|
# Move the cursor forward
|
||||||
|
self.cursor+=1
|
||||||
|
|
||||||
|
|
||||||
|
def get_value(self):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
def set_value(self, val):
|
||||||
|
self.value = int(val)
|
||||||
|
self.valstr = "%d"%self.value
|
||||||
|
self.cursor = len(self.valstr)
|
||||||
|
|
||||||
|
|
||||||
|
class FloatSpinInput(InputField):
|
||||||
|
def __init__(self, parent, message, name, move_func, value, inc_amt, precision, min_val, max_val):
|
||||||
|
self.parent = parent
|
||||||
|
self.message = message
|
||||||
|
self.name = name
|
||||||
|
self.precision = precision
|
||||||
|
self.inc_amt = inc_amt
|
||||||
|
self.value = round(float(value),self.precision)
|
||||||
|
self.initvalue = self.value
|
||||||
|
self.fmt = "%%.%df"%precision
|
||||||
|
self.valstr = self.fmt%self.value
|
||||||
|
self.cursor = len(self.valstr)
|
||||||
|
self.cursoff = len(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 render(self, screen, row, width, active, col=1, cursor_offset=0):
|
||||||
|
if not active and not self.valstr:
|
||||||
|
self.value = self.initvalue
|
||||||
|
self.valstr = self.fmt%self.value
|
||||||
|
self.cursor = len(self.valstr)
|
||||||
|
if not active and self.need_update:
|
||||||
|
self.value = round(float(self.valstr),self.precision)
|
||||||
|
self.valstr = self.fmt%self.value
|
||||||
|
self.cursor = len(self.valstr)
|
||||||
|
if not self.valstr:
|
||||||
|
self.parent.add_string(row,"%s [ ]"%self.message,screen,col,False,True)
|
||||||
|
elif active:
|
||||||
|
self.parent.add_string(row,"%s [ {!black,white,bold!}%s{!white,black!} ]"%(self.message,self.valstr),screen,col,False,True)
|
||||||
|
else:
|
||||||
|
self.parent.add_string(row,"%s [ %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:
|
||||||
|
self.value+=self.inc_amt
|
||||||
|
self.valstr = self.fmt%self.value
|
||||||
|
self.cursor = len(self.valstr)
|
||||||
|
elif c == curses.KEY_NPAGE:
|
||||||
|
self.value-=self.inc_amt
|
||||||
|
self.valstr = self.fmt%self.value
|
||||||
|
self.cursor = len(self.valstr)
|
||||||
|
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.value)
|
||||||
|
elif c == curses.KEY_BACKSPACE or c == 127:
|
||||||
|
if 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 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.cursor == 0 and self.min_val < 0:
|
||||||
|
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:
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
def set_value(self, val):
|
||||||
|
self.value = round(float(val),self.precision)
|
||||||
|
self.valstr = self.fmt%self.value
|
||||||
|
self.cursor = len(self.valstr)
|
||||||
|
|
||||||
|
|
||||||
|
class SelectInput(InputField):
|
||||||
|
def __init__(self, parent, message, name, opts, vals, selidx):
|
||||||
|
self.parent = parent
|
||||||
|
self.message = message
|
||||||
|
self.name = name
|
||||||
|
self.opts = opts
|
||||||
|
self.vals = vals
|
||||||
|
self.selidx = selidx
|
||||||
|
|
||||||
|
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:
|
||||||
|
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):
|
||||||
|
self.parent = parent
|
||||||
|
self.move_func = move_func
|
||||||
|
self.width = width
|
||||||
|
|
||||||
|
self.message = message
|
||||||
|
self.name = name
|
||||||
|
self.value = value
|
||||||
|
self.docmp = docmp
|
||||||
|
|
||||||
|
self.tab_count = 0
|
||||||
|
self.cursor = len(self.value)
|
||||||
|
self.opts = None
|
||||||
|
self.opt_off = 0
|
||||||
|
|
||||||
|
def render(self,screen,row,width,selected,col=1,cursor_offset=0):
|
||||||
|
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)
|
||||||
|
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 and second_hit: # display multiple options on second tab hit
|
||||||
|
self.opts = " ".join(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.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 += "/"
|
||||||
|
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 += "/"
|
||||||
|
ret.append(p)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
class InputPopup(Popup):
|
||||||
|
def __init__(self,parent_mode,title,width_req=-1,height_req=-1,close_cb=None):
|
||||||
|
Popup.__init__(self,parent_mode,title,width_req,height_req,close_cb)
|
||||||
|
self.inputs = []
|
||||||
|
self.current_input = 0
|
||||||
|
|
||||||
|
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.parent, self.move, self.width, message,
|
||||||
|
name, value, complete))
|
||||||
|
|
||||||
|
def add_select_input(self, message, name, opts, vals, default_index=0):
|
||||||
|
self.inputs.append(SelectInput(self.parent, message, name, opts, vals, default_index))
|
||||||
|
|
||||||
|
def add_checked_input(self, message, name, checked=False):
|
||||||
|
self.inputs.append(CheckedInput(self.parent,message,name,checked))
|
||||||
|
|
||||||
|
def _refresh_lines(self):
|
||||||
|
self._cursor_row = -1
|
||||||
|
self._cursor_col = -1
|
||||||
|
curses.curs_set(0)
|
||||||
|
crow = 1
|
||||||
|
for i,ipt in enumerate(self.inputs):
|
||||||
|
crow += ipt.render(self.screen,crow,self.width,i==self.current_input)
|
||||||
|
|
||||||
|
# need to do this last as adding things moves the cursor
|
||||||
|
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)
|
||||||
|
|
||||||
|
self.refresh()
|
||||||
|
return False
|
||||||
|
|
|
@ -0,0 +1,294 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# popup.py
|
||||||
|
#
|
||||||
|
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||||
|
#
|
||||||
|
# Deluge is free software.
|
||||||
|
#
|
||||||
|
# You may redistribute it and/or modify it under the terms of the
|
||||||
|
# GNU General Public License, as published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option)
|
||||||
|
# any later version.
|
||||||
|
#
|
||||||
|
# deluge is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
# See the GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with deluge. If not, write to:
|
||||||
|
# The Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin Street, Fifth Floor
|
||||||
|
# Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# In addition, as a special exception, the copyright holders give
|
||||||
|
# permission to link the code of portions of this program with the OpenSSL
|
||||||
|
# library.
|
||||||
|
# You must obey the GNU General Public License in all respects for all of
|
||||||
|
# the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
# exception, you may extend this exception to your version of the file(s),
|
||||||
|
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||||
|
# this exception statement from your version. If you delete this exception
|
||||||
|
# statement from all source files in the program, then also delete it here.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
try:
|
||||||
|
import curses
|
||||||
|
import signal
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
import logging
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class Popup:
|
||||||
|
def __init__(self,parent_mode,title,width_req=-1,height_req=-1,close_cb=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 _doRead on the popup instead of/in addition to
|
||||||
|
running its own _doRead 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
|
||||||
|
|
||||||
|
_doRead(self) - handle user input to the popup.
|
||||||
|
"""
|
||||||
|
self.parent = parent_mode
|
||||||
|
|
||||||
|
if (height_req <= 0):
|
||||||
|
height_req = int(self.parent.rows/2)
|
||||||
|
if (width_req <= 0):
|
||||||
|
width_req = int(self.parent.cols/2)
|
||||||
|
by = (self.parent.rows/2)-(height_req/2)
|
||||||
|
bx = (self.parent.cols/2)-(width_req/2)
|
||||||
|
self.screen = curses.newwin(height_req,width_req,by,bx)
|
||||||
|
|
||||||
|
self.title = title
|
||||||
|
self._close_cb = close_cb
|
||||||
|
self.height,self.width = self.screen.getmaxyx()
|
||||||
|
self._divider = None
|
||||||
|
self._lineoff = 0
|
||||||
|
self._lines = []
|
||||||
|
|
||||||
|
def _refresh_lines(self):
|
||||||
|
crow = 1
|
||||||
|
for row,line in enumerate(self._lines):
|
||||||
|
if (crow >= self.height-1):
|
||||||
|
break
|
||||||
|
if (row < self._lineoff):
|
||||||
|
continue
|
||||||
|
self.parent.add_string(crow,line,self.screen,1,False,True)
|
||||||
|
crow+=1
|
||||||
|
|
||||||
|
def handle_resize(self):
|
||||||
|
log.debug("Resizing popup window (actually, just creating a new one)")
|
||||||
|
self.screen = curses.newwin((self.parent.rows/2),(self.parent.cols/2),(self.parent.rows/4),(self.parent.cols/4))
|
||||||
|
self.height,self.width = self.screen.getmaxyx()
|
||||||
|
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
self.screen.clear()
|
||||||
|
self.screen.border(0,0,0,0)
|
||||||
|
toff = max(1,int((self.parent.cols/4)-(len(self.title)/2)))
|
||||||
|
self.parent.add_string(0,"{!white,black,bold!}%s"%self.title,self.screen,toff,False,True)
|
||||||
|
|
||||||
|
self._refresh_lines()
|
||||||
|
|
||||||
|
self.screen.redrawwin()
|
||||||
|
self.screen.noutrefresh()
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self._lines = []
|
||||||
|
|
||||||
|
def handle_read(self, c):
|
||||||
|
if c == curses.KEY_UP:
|
||||||
|
self._lineoff = max(0,self._lineoff -1)
|
||||||
|
elif c == curses.KEY_DOWN:
|
||||||
|
if len(self._lines)-self._lineoff > (self.height-2):
|
||||||
|
self._lineoff += 1
|
||||||
|
|
||||||
|
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):
|
||||||
|
Popup.__init__(self,parent_mode,title)
|
||||||
|
self._selection_callback = selection_callback
|
||||||
|
self._selection_args = args
|
||||||
|
self._selectable_lines = []
|
||||||
|
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 == None: fg = "black"
|
||||||
|
colorstr = "{!%s,white,bold!}"%fg
|
||||||
|
if udx >= 0:
|
||||||
|
ustr = "{!%s,white,bold,underline!}"%fg
|
||||||
|
else:
|
||||||
|
if fg == 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 handle_read(self, c):
|
||||||
|
if c == curses.KEY_UP:
|
||||||
|
#self._lineoff = max(0,self._lineoff -1)
|
||||||
|
if (self._selected != self._selectable_lines[0] and
|
||||||
|
len(self._selectable_lines) > 1):
|
||||||
|
idx = self._selectable_lines.index(self._selected)
|
||||||
|
self._selected = self._selectable_lines[idx-1]
|
||||||
|
elif c == curses.KEY_DOWN:
|
||||||
|
#if len(self._lines)-self._lineoff > (self.height-2):
|
||||||
|
# self._lineoff += 1
|
||||||
|
idx = self._selectable_lines.index(self._selected)
|
||||||
|
if (idx < len(self._selectable_lines)-1):
|
||||||
|
self._selected = self._selectable_lines[idx+1]
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
_strip_re = re.compile("\{!.*?!\}")
|
||||||
|
_min_height = 3
|
||||||
|
|
||||||
|
def __init__(self, parent_mode, title, message):
|
||||||
|
self.message = message
|
||||||
|
self.width= int(parent_mode.cols/2)
|
||||||
|
lns = self._split_message()
|
||||||
|
Popup.__init__(self,parent_mode,title,height_req=(len(lns)+2))
|
||||||
|
self._lines = lns
|
||||||
|
|
||||||
|
def _split_message(self):
|
||||||
|
ret = []
|
||||||
|
wl = (self.width-2)
|
||||||
|
|
||||||
|
s1 = self.message.split("\n")
|
||||||
|
|
||||||
|
for s in s1:
|
||||||
|
while len(self._strip_re.sub('',s)) > wl:
|
||||||
|
sidx = s.rfind(" ",0,wl-1)
|
||||||
|
sidx += 1
|
||||||
|
if sidx > 0:
|
||||||
|
ret.append(s[0:sidx])
|
||||||
|
s = s[sidx:]
|
||||||
|
else:
|
||||||
|
# can't find a reasonable split, just split at width
|
||||||
|
ret.append(s[0:wl])
|
||||||
|
s = s[wl:]
|
||||||
|
if s:
|
||||||
|
ret.append(s)
|
||||||
|
|
||||||
|
for i in range(len(ret),self._min_height):
|
||||||
|
ret.append(" ")
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def handle_resize(self):
|
||||||
|
Popup.handle_resize(self)
|
||||||
|
self.clear()
|
||||||
|
self._lines = self._split_message()
|
|
@ -0,0 +1,370 @@
|
||||||
|
#
|
||||||
|
# preference_panes.py
|
||||||
|
#
|
||||||
|
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||||
|
#
|
||||||
|
# Deluge is free software.
|
||||||
|
#
|
||||||
|
# You may redistribute it and/or modify it under the terms of the
|
||||||
|
# GNU General Public License, as published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option)
|
||||||
|
# any later version.
|
||||||
|
#
|
||||||
|
# deluge is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
# See the GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with deluge. If not, write to:
|
||||||
|
# The Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin Street, Fifth Floor
|
||||||
|
# Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# In addition, as a special exception, the copyright holders give
|
||||||
|
# permission to link the code of portions of this program with the OpenSSL
|
||||||
|
# library.
|
||||||
|
# You must obey the GNU General Public License in all respects for all of
|
||||||
|
# the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
# exception, you may extend this exception to your version of the file(s),
|
||||||
|
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||||
|
# this exception statement from your version. If you delete this exception
|
||||||
|
# statement from all source files in the program, then also delete it here.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
from deluge.ui.console.modes.input_popup import TextInput,SelectInput,CheckedInput,IntSpinInput,FloatSpinInput,CheckedPlusInput
|
||||||
|
|
||||||
|
try:
|
||||||
|
import curses
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
import logging
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class NoInput:
|
||||||
|
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 type(v) == float:
|
||||||
|
self.txt = "%s %.2f"%(self.label,self.value)
|
||||||
|
else:
|
||||||
|
self.txt = "%s %s"%(self.label,self.value)
|
||||||
|
|
||||||
|
class BasePane:
|
||||||
|
def __init__(self, offset, parent, width):
|
||||||
|
self.offset = offset+1
|
||||||
|
self.parent = parent
|
||||||
|
self.width = width
|
||||||
|
self.inputs = []
|
||||||
|
self.active_input = -1
|
||||||
|
|
||||||
|
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_ports_to","listen_ports_from",
|
||||||
|
"out_ports_from","out_ports_to"):
|
||||||
|
if ipt.name == "listen_ports_to":
|
||||||
|
conf_dict["listen_ports"] = (self.infrom.get_value(),self.into.get_value())
|
||||||
|
if ipt.name == "out_ports_to":
|
||||||
|
conf_dict["outgoing_ports"] = (self.outfrom.get_value(),self.outto.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
|
||||||
|
crow = 1
|
||||||
|
for i,ipt in enumerate(self.inputs):
|
||||||
|
if ipt.depend_skip():
|
||||||
|
continue
|
||||||
|
act = active and i==self.active_input
|
||||||
|
crow += ipt.render(screen,crow,width, act, self.offset)
|
||||||
|
|
||||||
|
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 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"])
|
||||||
|
autotxt = TextInput(self.parent,self.move,self.width,None,"autoadd_location",parent.core_config["autoadd_location"],False)
|
||||||
|
self.add_checkedplus_input("autoadd_enable","Auto add .torrents from:",autotxt,parent.core_config["autoadd_enable"])
|
||||||
|
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("Allocation",True)
|
||||||
|
|
||||||
|
if parent.core_config["compact_allocation"]:
|
||||||
|
alloc_idx = 1
|
||||||
|
else:
|
||||||
|
alloc_idx = 0
|
||||||
|
self.add_select_input("compact_allocation",None,["Use Full Allocation","Use Compact Allocation"],[False,True],alloc_idx)
|
||||||
|
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("add_paused","Add torrents in paused state",parent.core_config["add_paused"])
|
||||||
|
|
||||||
|
|
||||||
|
class NetworkPane(BasePane):
|
||||||
|
def __init__(self, offset, parent, width):
|
||||||
|
BasePane.__init__(self,offset,parent,width)
|
||||||
|
self.add_header("Incomming Ports")
|
||||||
|
inrand = CheckedInput(parent,"Use Random Ports 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," From:","listen_ports_from",self.move,listen_ports[0],0,65535)
|
||||||
|
self.infrom.set_depend(inrand,True)
|
||||||
|
self.into = IntSpinInput(self.parent," To: ","listen_ports_to",self.move,listen_ports[1],0,65535)
|
||||||
|
self.into.set_depend(inrand,True)
|
||||||
|
self.inputs.append(self.infrom)
|
||||||
|
self.inputs.append(self.into)
|
||||||
|
|
||||||
|
|
||||||
|
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("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"])
|
||||||
|
self.add_checked_input("enc_prefer_rc4","Encrypt Entire Stream",parent.core_config["enc_prefer_rc4"])
|
||||||
|
|
||||||
|
|
||||||
|
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 InterfacePane(BasePane):
|
||||||
|
def __init__(self, offset, parent, width):
|
||||||
|
BasePane.__init__(self,offset,parent,width)
|
||||||
|
self.add_header("Interface Settings Comming Soon")
|
||||||
|
# add title bar control here
|
||||||
|
|
||||||
|
|
||||||
|
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("General")
|
||||||
|
self.add_checked_input("queue_new_to_top","Queue new torrents to top",parent.core_config["queue_new_to_top"])
|
||||||
|
self.add_header("Active Torrents",True)
|
||||||
|
self.add_int_spin_input("max_active_limit","Total active:",parent.core_config["max_active_limit"],-1,9999)
|
||||||
|
self.add_int_spin_input("max_active_downloading","Total active downloading:",parent.core_config["max_active_downloading"],-1,9999)
|
||||||
|
self.add_int_spin_input("max_active_seeding","Total active seeding:",parent.core_config["max_active_seeding"],-1,9999)
|
||||||
|
self.add_checked_input("dont_count_slow_torrents","Do not count slow torrents",parent.core_config["dont_count_slow_torrents"])
|
||||||
|
self.add_header("Seeding",True)
|
||||||
|
self.add_float_spin_input("share_ratio_limit","Share Ratio Limit:",parent.core_config["share_ratio_limit"],1.0,2,-1.0,100.0)
|
||||||
|
self.add_float_spin_input("seed_time_ratio_limit","Share Time Ratio:",parent.core_config["seed_time_ratio_limit"],1.0,2,-1.0,100.0)
|
||||||
|
self.add_int_spin_input("seed_time_limit","Seed 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","Stop seeding when share ratio reaches:",seedratio,parent.core_config["stop_seed_at_ratio"])
|
||||||
|
self.add_checked_input("remove_seed_at_ratio","Remove torrent when share ratio reached",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 Comming Soon")
|
||||||
|
|
||||||
|
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
|
|
@ -0,0 +1,288 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# preferences.py
|
||||||
|
#
|
||||||
|
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||||
|
#
|
||||||
|
# Deluge is free software.
|
||||||
|
#
|
||||||
|
# You may redistribute it and/or modify it under the terms of the
|
||||||
|
# GNU General Public License, as published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option)
|
||||||
|
# any later version.
|
||||||
|
#
|
||||||
|
# deluge is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
# See the GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with deluge. If not, write to:
|
||||||
|
# The Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin Street, Fifth Floor
|
||||||
|
# Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# In addition, as a special exception, the copyright holders give
|
||||||
|
# permission to link the code of portions of this program with the OpenSSL
|
||||||
|
# library.
|
||||||
|
# You must obey the GNU General Public License in all respects for all of
|
||||||
|
# the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
# exception, you may extend this exception to your version of the file(s),
|
||||||
|
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||||
|
# this exception statement from your version. If you delete this exception
|
||||||
|
# statement from all source files in the program, then also delete it here.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
import deluge.component as component
|
||||||
|
from deluge.ui.client import client
|
||||||
|
from basemode import BaseMode
|
||||||
|
from input_popup import Popup,SelectInput
|
||||||
|
|
||||||
|
from preference_panes import DownloadsPane,NetworkPane,BandwidthPane,InterfacePane
|
||||||
|
from preference_panes import OtherPane,DaemonPane,QueuePane,ProxyPane,CachePane
|
||||||
|
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
try:
|
||||||
|
import curses
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
import logging
|
||||||
|
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:
|
||||||
|
CATEGORIES = 0
|
||||||
|
PREFRENCES = 1
|
||||||
|
ACTIONS = 2
|
||||||
|
|
||||||
|
class Preferences(BaseMode):
|
||||||
|
def __init__(self, parent_mode, core_config, active_port, status, stdscr, encoding=None):
|
||||||
|
self.parent_mode = parent_mode
|
||||||
|
self.categories = [_("Downloads"), _("Network"), _("Bandwidth"),
|
||||||
|
_("Interface"), _("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.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.prefs_width = self.cols-self.div_off-1
|
||||||
|
self.panes = [
|
||||||
|
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),
|
||||||
|
InterfacePane(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)
|
||||||
|
]
|
||||||
|
|
||||||
|
self.action_input = SelectInput(self,None,None,["Cancel","Apply","OK"],[0,1,2],0)
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
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 refresh(self):
|
||||||
|
if self.popup == None and self.messages:
|
||||||
|
title,msg = self.messages.popleft()
|
||||||
|
self.popup = MessagePopup(self,title,msg)
|
||||||
|
|
||||||
|
self.stdscr.clear()
|
||||||
|
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()
|
||||||
|
|
||||||
|
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:
|
||||||
|
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)
|
||||||
|
|
||||||
|
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.clear()
|
||||||
|
component.get("ConsoleUI").set_mode(self.parent_mode)
|
||||||
|
self.parent_mode.resume()
|
||||||
|
|
||||||
|
def _doRead(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 == 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()
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
# torrent_actions.py
|
||||||
|
#
|
||||||
|
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||||
|
#
|
||||||
|
# Deluge is free software.
|
||||||
|
#
|
||||||
|
# You may redistribute it and/or modify it under the terms of the
|
||||||
|
# GNU General Public License, as published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option)
|
||||||
|
# any later version.
|
||||||
|
#
|
||||||
|
# deluge is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
# See the GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with deluge. If not, write to:
|
||||||
|
# The Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin Street, Fifth Floor
|
||||||
|
# Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# In addition, as a special exception, the copyright holders give
|
||||||
|
# permission to link the code of portions of this program with the OpenSSL
|
||||||
|
# library.
|
||||||
|
# You must obey the GNU General Public License in all respects for all of
|
||||||
|
# the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
# exception, you may extend this exception to your version of the file(s),
|
||||||
|
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||||
|
# this exception statement from your version. If you delete this exception
|
||||||
|
# statement from all source files in the program, then also delete it here.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
from deluge.ui.client import client
|
||||||
|
from popup import SelectablePopup
|
||||||
|
|
||||||
|
import logging
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class ACTION:
|
||||||
|
PAUSE=0
|
||||||
|
RESUME=1
|
||||||
|
REANNOUNCE=2
|
||||||
|
EDIT_TRACKERS=3
|
||||||
|
RECHECK=4
|
||||||
|
REMOVE=5
|
||||||
|
|
||||||
|
REMOVE_DATA=6
|
||||||
|
REMOVE_NODATA=7
|
||||||
|
|
||||||
|
DETAILS=8
|
||||||
|
|
||||||
|
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.REMOVE:
|
||||||
|
def do_remove(idx,data,mode,ids):
|
||||||
|
if data:
|
||||||
|
wd = data==ACTION.REMOVE_DATA
|
||||||
|
for tid in ids:
|
||||||
|
log.debug("Removing torrent: %s,%d",tid,wd)
|
||||||
|
client.core.remove_torrent(tid,wd).addErrback(action_error,mode)
|
||||||
|
if len(ids) == 1:
|
||||||
|
mode.clear_marks()
|
||||||
|
return True
|
||||||
|
popup = SelectablePopup(mode,"Confirm Remove",do_remove,mode,ids)
|
||||||
|
popup.add_line("Are you sure you want to remove the marked torrents?",selectable=False)
|
||||||
|
popup.add_line("Remove with _data",data=ACTION.REMOVE_DATA)
|
||||||
|
popup.add_line("Remove _torrent",data=ACTION.REMOVE_NODATA)
|
||||||
|
popup.add_line("_Cancel",data=0)
|
||||||
|
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")
|
||||||
|
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):
|
||||||
|
popup = SelectablePopup(mode,"Torrent Actions",torrent_action,mode,tids)
|
||||||
|
popup.add_line("_Pause",data=ACTION.PAUSE)
|
||||||
|
popup.add_line("_Resume",data=ACTION.RESUME)
|
||||||
|
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)
|
||||||
|
if details:
|
||||||
|
popup.add_divider()
|
||||||
|
popup.add_line("Torrent _Details",data=ACTION.DETAILS)
|
||||||
|
mode.set_popup(popup)
|
|
@ -0,0 +1,475 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# torrentdetail.py
|
||||||
|
#
|
||||||
|
# Copyright (C) 2011 Nick Lanham <nick@afternight.org>
|
||||||
|
#
|
||||||
|
# Deluge is free software.
|
||||||
|
#
|
||||||
|
# You may redistribute it and/or modify it under the terms of the
|
||||||
|
# GNU General Public License, as published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option)
|
||||||
|
# any later version.
|
||||||
|
#
|
||||||
|
# deluge is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
# See the GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with deluge. If not, write to:
|
||||||
|
# The Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin Street, Fifth Floor
|
||||||
|
# Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
# In addition, as a special exception, the copyright holders give
|
||||||
|
# permission to link the code of portions of this program with the OpenSSL
|
||||||
|
# library.
|
||||||
|
# You must obey the GNU General Public License in all respects for all of
|
||||||
|
# the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
# exception, you may extend this exception to your version of the file(s),
|
||||||
|
# but you are not obligated to do so. If you do not wish to do so, delete
|
||||||
|
# this exception statement from your version. If you delete this exception
|
||||||
|
# statement from all source files in the program, then also delete it here.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
import deluge.component as component
|
||||||
|
from basemode import BaseMode
|
||||||
|
import deluge.common
|
||||||
|
from deluge.ui.client import client
|
||||||
|
|
||||||
|
from sys import maxint
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
from deluge.ui.sessionproxy import SessionProxy
|
||||||
|
|
||||||
|
from popup import Popup,SelectablePopup,MessagePopup
|
||||||
|
from add_util import add_torrent
|
||||||
|
from input_popup import InputPopup
|
||||||
|
import format_utils
|
||||||
|
|
||||||
|
from torrent_actions import torrent_actions_popup
|
||||||
|
|
||||||
|
try:
|
||||||
|
import curses
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
import logging
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class TorrentDetail(BaseMode, component.Component):
|
||||||
|
def __init__(self, alltorrentmode, torrentid, stdscr, encoding=None):
|
||||||
|
self.alltorrentmode = alltorrentmode
|
||||||
|
self.torrentid = torrentid
|
||||||
|
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","save_path","file_progress","file_priorities","message"]
|
||||||
|
self._info_fields = [
|
||||||
|
("Name",None,("name",)),
|
||||||
|
("State", None, ("state",)),
|
||||||
|
("Status",None,("message",)),
|
||||||
|
("Down Speed", format_utils.format_speed, ("download_payload_rate",)),
|
||||||
|
("Up Speed", format_utils.format_speed, ("upload_payload_rate",)),
|
||||||
|
("Progress", format_utils.format_progress, ("progress",)),
|
||||||
|
("ETA", deluge.common.ftime, ("eta",)),
|
||||||
|
("Path", None, ("save_path",)),
|
||||||
|
("Downloaded",deluge.common.fsize,("all_time_download",)),
|
||||||
|
("Uploaded", deluge.common.fsize,("total_uploaded",)),
|
||||||
|
("Share Ratio", lambda x:x < 0 and "∞" or "%.3f"%x, ("ratio",)),
|
||||||
|
("Seeders",format_utils.format_seeds_peers,("num_seeds","total_seeds")),
|
||||||
|
("Peers",format_utils.format_seeds_peers,("num_peers","total_peers")),
|
||||||
|
("Active Time",deluge.common.ftime,("active_time",)),
|
||||||
|
("Seeding Time",deluge.common.ftime,("seeding_time",)),
|
||||||
|
("Date Added",deluge.common.fdate,("time_added",)),
|
||||||
|
("Availability", lambda x:x < 0 and "∞" or "%.3f"%x, ("distributed_copies",)),
|
||||||
|
("Pieces", format_utils.format_pieces, ("num_pieces","piece_length")),
|
||||||
|
]
|
||||||
|
self.file_list = None
|
||||||
|
self.current_file = None
|
||||||
|
self.current_file_idx = 0
|
||||||
|
self.file_limit = maxint
|
||||||
|
self.file_off = 0
|
||||||
|
self.more_to_draw = False
|
||||||
|
|
||||||
|
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"])
|
||||||
|
curses.curs_set(0)
|
||||||
|
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 update(self):
|
||||||
|
component.get("SessionProxy").get_torrent_status(self.torrentid, self._status_keys).addCallback(self.set_state)
|
||||||
|
|
||||||
|
def set_state(self, state):
|
||||||
|
log.debug("got state")
|
||||||
|
if not self.file_list:
|
||||||
|
# don't keep getting the files once we've got them once
|
||||||
|
self.files_sep = "{!green,black,bold,underline!}%s"%(("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")
|
||||||
|
self._fill_progress(self.file_list,state["file_progress"])
|
||||||
|
for i,prio in enumerate(state["file_priorities"]):
|
||||||
|
self.file_dict[i][6] = prio
|
||||||
|
del state["file_progress"]
|
||||||
|
del state["file_priorities"]
|
||||||
|
self.torrent_state = state
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
# split file list into directory tree. this function assumes all files in a
|
||||||
|
# particular directory are returned together. it won't work otherwise.
|
||||||
|
# returned list is a list of lists of the form:
|
||||||
|
# [file/dir_name,index,size,children,expanded,progress,priority]
|
||||||
|
# for directories index values count down from maxint (for marking usage),
|
||||||
|
# for files the index is the value returned in the
|
||||||
|
# state object for use with other libtorrent calls (i.e. setting prio)
|
||||||
|
#
|
||||||
|
# Also returns a dictionary that maps index values to the file leaves
|
||||||
|
# for fast updating of progress and priorities
|
||||||
|
def build_file_list(self, file_tuples,prog,prio):
|
||||||
|
ret = []
|
||||||
|
retdict = {}
|
||||||
|
diridx = maxint
|
||||||
|
for f in file_tuples:
|
||||||
|
cur = ret
|
||||||
|
ps = f["path"].split("/")
|
||||||
|
fin = ps[-1]
|
||||||
|
for p in ps:
|
||||||
|
if not cur or p != cur[-1][0]:
|
||||||
|
cl = []
|
||||||
|
if p == fin:
|
||||||
|
ent = [p,f["index"],f["size"],cl,False,
|
||||||
|
format_utils.format_progress(prog[f["index"]]*100),
|
||||||
|
prio[f["index"]]]
|
||||||
|
retdict[f["index"]] = ent
|
||||||
|
else:
|
||||||
|
ent = [p,diridx,-1,cl,False,0,-1]
|
||||||
|
retdict[diridx] = ent
|
||||||
|
diridx-=1
|
||||||
|
cur.append(ent)
|
||||||
|
cur = cl
|
||||||
|
else:
|
||||||
|
cur = cur[-1][3]
|
||||||
|
self._build_sizes(ret)
|
||||||
|
self._fill_progress(ret,prog)
|
||||||
|
return (ret,retdict)
|
||||||
|
|
||||||
|
# fill in the sizes of the directory entries based on their children
|
||||||
|
def _build_sizes(self, fs):
|
||||||
|
ret = 0
|
||||||
|
for f in fs:
|
||||||
|
if f[2] == -1:
|
||||||
|
val = self._build_sizes(f[3])
|
||||||
|
ret += val
|
||||||
|
f[2] = val
|
||||||
|
else:
|
||||||
|
ret += f[2]
|
||||||
|
return ret
|
||||||
|
|
||||||
|
# fills in progress fields in all entries based on progs
|
||||||
|
# returns the # of bytes complete in all the children of fs
|
||||||
|
def _fill_progress(self,fs,progs):
|
||||||
|
tb = 0
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
tb += bd
|
||||||
|
return tb
|
||||||
|
|
||||||
|
def _update_columns(self):
|
||||||
|
self.column_widths = [-1,15,15,20]
|
||||||
|
req = sum(filter(lambda x:x >= 0,self.column_widths))
|
||||||
|
if (req > self.cols): # can't satisfy requests, just spread out evenly
|
||||||
|
cw = int(self.cols/len(self.column_names))
|
||||||
|
for i in range(0,len(self.column_widths)):
|
||||||
|
self.column_widths[i] = cw
|
||||||
|
else:
|
||||||
|
rem = self.cols - req
|
||||||
|
var_cols = len(filter(lambda x: x < 0,self.column_widths))
|
||||||
|
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 = "{!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 draw_files(self,files,depth,off,idx):
|
||||||
|
for fl in files:
|
||||||
|
# kick out if we're going to draw too low on the screen
|
||||||
|
if (off >= self.rows-1):
|
||||||
|
self.more_to_draw = True
|
||||||
|
return -1,-1
|
||||||
|
|
||||||
|
self.file_limit = idx
|
||||||
|
|
||||||
|
if idx >= self.file_off:
|
||||||
|
# set fg/bg colors based on if we are selected/marked or not
|
||||||
|
|
||||||
|
# default values
|
||||||
|
fg = "white"
|
||||||
|
bg = "black"
|
||||||
|
|
||||||
|
if fl[1] in self.marked:
|
||||||
|
bg = "blue"
|
||||||
|
|
||||||
|
if idx == self.current_file_idx:
|
||||||
|
self.current_file = fl
|
||||||
|
bg = "white"
|
||||||
|
if fl[1] in self.marked:
|
||||||
|
fg = "blue"
|
||||||
|
else:
|
||||||
|
fg = "black"
|
||||||
|
|
||||||
|
color_string = "{!%s,%s!}"%(fg,bg)
|
||||||
|
|
||||||
|
#actually draw the dir/file string
|
||||||
|
if fl[3] and fl[4]: # this is an expanded directory
|
||||||
|
xchar = 'v'
|
||||||
|
elif fl[3]: # collapsed directory
|
||||||
|
xchar = '>'
|
||||||
|
else: # file
|
||||||
|
xchar = '-'
|
||||||
|
|
||||||
|
r = format_utils.format_row(["%s%s %s"%(" "*depth,xchar,fl[0]),
|
||||||
|
deluge.common.fsize(fl[2]),fl[5],
|
||||||
|
format_utils.format_priority(fl[6])],
|
||||||
|
self.column_widths)
|
||||||
|
|
||||||
|
self.add_string(off,"%s%s"%(color_string,r),trim=False)
|
||||||
|
off += 1
|
||||||
|
|
||||||
|
if fl[3] and fl[4]:
|
||||||
|
# recurse if we have children and are expanded
|
||||||
|
off,idx = self.draw_files(fl[3],depth+1,off,idx+1)
|
||||||
|
if off < 0: return (off,idx)
|
||||||
|
else:
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
return (off,idx)
|
||||||
|
|
||||||
|
def on_resize(self, *args):
|
||||||
|
BaseMode.on_resize_norefresh(self, *args)
|
||||||
|
self._update_columns()
|
||||||
|
if self.popup:
|
||||||
|
self.popup.handle_resize()
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
def refresh(self,lines=None):
|
||||||
|
# show a message popup if there's anything queued
|
||||||
|
if self.popup == None and self.messages:
|
||||||
|
title,msg = self.messages.popleft()
|
||||||
|
self.popup = MessagePopup(self,title,msg)
|
||||||
|
|
||||||
|
# Update the status bars
|
||||||
|
self.stdscr.clear()
|
||||||
|
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))
|
||||||
|
|
||||||
|
if self.files_sep:
|
||||||
|
self.add_string((self.rows/2)-1,self.files_sep)
|
||||||
|
|
||||||
|
off = 1
|
||||||
|
if self.torrent_state:
|
||||||
|
for f in self._info_fields:
|
||||||
|
if off >= (self.rows/2): break
|
||||||
|
if f[1] != None:
|
||||||
|
args = []
|
||||||
|
try:
|
||||||
|
for key in f[2]:
|
||||||
|
args.append(self.torrent_state[key])
|
||||||
|
except:
|
||||||
|
log.debug("Could not get info field: %s",e)
|
||||||
|
continue
|
||||||
|
info = f[1](*args)
|
||||||
|
else:
|
||||||
|
info = self.torrent_state[f[2][0]]
|
||||||
|
|
||||||
|
self.add_string(off,"{!info!}%s: {!input!}%s"%(f[0],info))
|
||||||
|
off += 1
|
||||||
|
else:
|
||||||
|
self.add_string(1, "Waiting for torrent state")
|
||||||
|
|
||||||
|
off = self.rows/2
|
||||||
|
self.add_string(off,self.column_string)
|
||||||
|
if self.file_list:
|
||||||
|
off += 1
|
||||||
|
self.more_to_draw = False
|
||||||
|
self.draw_files(self.file_list,0,off,0)
|
||||||
|
|
||||||
|
#self.stdscr.redrawwin()
|
||||||
|
self.stdscr.noutrefresh()
|
||||||
|
|
||||||
|
if self.popup:
|
||||||
|
self.popup.refresh()
|
||||||
|
|
||||||
|
curses.doupdate()
|
||||||
|
|
||||||
|
# expand or collapse the current file
|
||||||
|
def expcol_cur_file(self):
|
||||||
|
self.current_file[4] = not self.current_file[4]
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
def file_list_down(self):
|
||||||
|
if (self.current_file_idx + 1) > self.file_limit:
|
||||||
|
if self.more_to_draw:
|
||||||
|
self.current_file_idx += 1
|
||||||
|
self.file_off += 1
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
self.current_file_idx += 1
|
||||||
|
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
def file_list_up(self):
|
||||||
|
self.current_file_idx = max(0,self.current_file_idx-1)
|
||||||
|
self.file_off = min(self.file_off,self.current_file_idx)
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
def back_to_overview(self):
|
||||||
|
component.stop(["TorrentDetail"])
|
||||||
|
component.deregister("TorrentDetail")
|
||||||
|
self.stdscr.clear()
|
||||||
|
component.get("ConsoleUI").set_mode(self.alltorrentmode)
|
||||||
|
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):
|
||||||
|
# has a priority been set on my parent (if so, I inherit it)
|
||||||
|
for f in files:
|
||||||
|
if f[3]: # dir, check if i'm setting on whole dir, then recurse
|
||||||
|
if f[1] in self.marked: # marked, recurse and update all children with new prio
|
||||||
|
parent_prio = selected_prio
|
||||||
|
self.build_prio_list(f[3],ret_list,parent_prio,selected_prio)
|
||||||
|
parent_prio = -1
|
||||||
|
else: # not marked, just recurse
|
||||||
|
self.build_prio_list(f[3],ret_list,parent_prio,selected_prio)
|
||||||
|
else: # file, need to add to list
|
||||||
|
if f[1] in self.marked or parent_prio >= 0:
|
||||||
|
# selected (or parent selected), use requested priority
|
||||||
|
ret_list.append((f[1],selected_prio))
|
||||||
|
else:
|
||||||
|
# not selected, just keep old priority
|
||||||
|
ret_list.append((f[1],f[6]))
|
||||||
|
|
||||||
|
def do_priority(self, idx, data):
|
||||||
|
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 len(self.marked) == 1:
|
||||||
|
self.marked = {}
|
||||||
|
return True
|
||||||
|
|
||||||
|
# show popup for priority selections
|
||||||
|
def show_priority_popup(self):
|
||||||
|
if self.marked:
|
||||||
|
self.popup = SelectablePopup(self,"Set File Priority",self.do_priority)
|
||||||
|
self.popup.add_line("_Do Not Download",data=deluge.common.FILE_PRIORITY["Do Not Download"])
|
||||||
|
self.popup.add_line("_Normal Priority",data=deluge.common.FILE_PRIORITY["Normal Priority"])
|
||||||
|
self.popup.add_line("_High Priority",data=deluge.common.FILE_PRIORITY["High Priority"])
|
||||||
|
self.popup.add_line("H_ighest Priority",data=deluge.common.FILE_PRIORITY["Highest Priority"])
|
||||||
|
|
||||||
|
def _mark_unmark(self,idx):
|
||||||
|
if idx in self.marked:
|
||||||
|
del self.marked[idx]
|
||||||
|
else:
|
||||||
|
self.marked[idx] = True
|
||||||
|
|
||||||
|
def _doRead(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) == 'q':
|
||||||
|
self.back_to_overview()
|
||||||
|
return
|
||||||
|
|
||||||
|
if c == 27 or c == curses.KEY_LEFT:
|
||||||
|
self.back_to_overview()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Navigate the torrent list
|
||||||
|
if c == curses.KEY_UP:
|
||||||
|
self.file_list_up()
|
||||||
|
elif c == curses.KEY_PPAGE:
|
||||||
|
pass
|
||||||
|
elif c == curses.KEY_DOWN:
|
||||||
|
self.file_list_down()
|
||||||
|
elif c == curses.KEY_NPAGE:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Enter Key
|
||||||
|
elif c == curses.KEY_ENTER or c == 10:
|
||||||
|
self.marked[self.current_file[1]] = True
|
||||||
|
self.show_priority_popup()
|
||||||
|
|
||||||
|
# space
|
||||||
|
elif c == 32:
|
||||||
|
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])
|
||||||
|
if chr(c) == 'c':
|
||||||
|
self.marked = {}
|
||||||
|
if chr(c) == 'a':
|
||||||
|
torrent_actions_popup(self,[self.torrentid],details=False)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.refresh()
|
|
@ -41,7 +41,6 @@ class StatusBars(component.Component):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
component.Component.__init__(self, "StatusBars", 2, depend=["CoreConfig"])
|
component.Component.__init__(self, "StatusBars", 2, depend=["CoreConfig"])
|
||||||
self.config = component.get("CoreConfig")
|
self.config = component.get("CoreConfig")
|
||||||
self.screen = component.get("ConsoleUI").screen
|
|
||||||
|
|
||||||
# Hold some values we get from the core
|
# Hold some values we get from the core
|
||||||
self.connections = 0
|
self.connections = 0
|
||||||
|
@ -49,6 +48,10 @@ class StatusBars(component.Component):
|
||||||
self.upload = ""
|
self.upload = ""
|
||||||
self.dht = 0
|
self.dht = 0
|
||||||
|
|
||||||
|
# Default values
|
||||||
|
self.topbar = "{!status!}Deluge %s Console - " % deluge.common.get_version()
|
||||||
|
self.bottombar = "{!status!}C: %s" % self.connections
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
|
@ -77,30 +80,28 @@ class StatusBars(component.Component):
|
||||||
|
|
||||||
def update_statusbars(self):
|
def update_statusbars(self):
|
||||||
# Update the topbar string
|
# Update the topbar string
|
||||||
self.screen.topbar = "{!status!}Deluge %s Console - " % deluge.common.get_version()
|
self.topbar = "{!status!}Deluge %s Console - " % deluge.common.get_version()
|
||||||
if client.connected():
|
if client.connected():
|
||||||
info = client.connection_info()
|
info = client.connection_info()
|
||||||
self.screen.topbar += "%s@%s:%s" % (info[2], info[0], info[1])
|
self.topbar += "%s@%s:%s" % (info[2], info[0], info[1])
|
||||||
else:
|
else:
|
||||||
self.screen.topbar += "Not Connected"
|
self.topbar += "Not Connected"
|
||||||
|
|
||||||
# Update the bottombar string
|
# Update the bottombar string
|
||||||
self.screen.bottombar = "{!status!}C: %s" % self.connections
|
self.bottombar = "{!status!}C: %s" % self.connections
|
||||||
|
|
||||||
if self.config["max_connections_global"] > -1:
|
if self.config["max_connections_global"] > -1:
|
||||||
self.screen.bottombar += " (%s)" % self.config["max_connections_global"]
|
self.bottombar += " (%s)" % self.config["max_connections_global"]
|
||||||
|
|
||||||
self.screen.bottombar += " D: %s/s" % self.download
|
self.bottombar += " D: %s/s" % self.download
|
||||||
|
|
||||||
if self.config["max_download_speed"] > -1:
|
if self.config["max_download_speed"] > -1:
|
||||||
self.screen.bottombar += " (%s KiB/s)" % self.config["max_download_speed"]
|
self.bottombar += " (%s KiB/s)" % self.config["max_download_speed"]
|
||||||
|
|
||||||
self.screen.bottombar += " U: %s/s" % self.upload
|
self.bottombar += " U: %s/s" % self.upload
|
||||||
|
|
||||||
if self.config["max_upload_speed"] > -1:
|
if self.config["max_upload_speed"] > -1:
|
||||||
self.screen.bottombar += " (%s KiB/s)" % self.config["max_upload_speed"]
|
self.bottombar += " (%s KiB/s)" % self.config["max_upload_speed"]
|
||||||
|
|
||||||
if self.config["dht"]:
|
if self.config["dht"]:
|
||||||
self.screen.bottombar += " DHT: %s" % self.dht
|
self.bottombar += " DHT: %s" % self.dht
|
||||||
|
|
||||||
self.screen.refresh()
|
|
||||||
|
|
Loading…
Reference in New Issue