Merge branch 'newconsole'

Conflicts:
	deluge/ui/console/main.py
This commit is contained in:
Nick 2011-02-22 00:26:57 +01:00
commit 5f8eda9204
22 changed files with 3928 additions and 254 deletions

View File

@ -61,7 +61,12 @@ schemes = {
"info": ("white", "black", "bold"),
"error": ("red", "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
@ -94,6 +99,14 @@ def init_colors():
curses.init_pair(counter, getattr(curses, fg), getattr(curses, bg))
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):
pass

View File

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

View File

@ -47,4 +47,6 @@ class Command(BaseCommand):
for key, value in status.items():
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

View File

@ -44,7 +44,7 @@ import deluge.component as component
class Command(BaseCommand):
"""Enable and disable debugging"""
usage = 'debug [on|off]'
usage = 'Usage: debug [on|off]'
def handle(self, state='', **options):
if state == 'on':
deluge.log.setLoggerLevel("debug")

View File

@ -39,7 +39,8 @@ from deluge.ui.client import client
import deluge.component as component
class Command(BaseCommand):
"Shutdown the deluge server."
"Shutdown the deluge server"
usage = "Usage: halt"
def handle(self, **options):
self.console = component.get("ConsoleUI")

View File

@ -85,7 +85,7 @@ def format_progressbar(progress, width):
s = "["
p = int(round((progress/100) * w))
s += "#" * p
s += "~" * (w - p)
s += "-" * (w - p)
s += "]"
return s

View File

@ -40,6 +40,7 @@ from twisted.internet import reactor
class Command(BaseCommand):
"""Exit from the client."""
aliases = ['exit']
interactive_only = True
def handle(self, *args, **options):
if client.connected():
def on_disconnect(result):

View File

@ -43,16 +43,17 @@ import locale
from twisted.internet import defer, reactor
from deluge.ui.console import UI_PATH
import deluge.component as component
from deluge.ui.client import client
import deluge.common
from deluge.ui.coreconfig import CoreConfig
from deluge.ui.sessionproxy import SessionProxy
from deluge.ui.console.statusbars import StatusBars
from deluge.ui.console.eventlog import EventLog
import screen
#import screen
import colors
from deluge.ui.ui import _UI
from deluge.ui.console import UI_PATH
log = logging.getLogger(__name__)
@ -62,16 +63,62 @@ class Console(_UI):
def __init__(self):
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",
"\n".join(cmds.keys()))
group.add_option("-d","--daemon",dest="daemon_addr",
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.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):
super(Console, self).start()
ConsoleUI(self.args)
ConsoleUI(self.args,self.cmds,(self.options.daemon_addr,
self.options.daemon_port,self.options.daemon_user,
self.options.daemon_pass))
def start():
Console().start()
@ -92,9 +139,11 @@ class OptionParser(optparse.OptionParser):
"""
raise Exception(msg)
class BaseCommand(object):
usage = 'usage'
interactive_only = False
option_list = tuple()
aliases = []
@ -122,6 +171,7 @@ class BaseCommand(object):
epilog = self.epilog,
option_list = self.option_list)
def load_commands(command_dir, exclude=[]):
def get_command(name):
return getattr(__import__('deluge.ui.console.commands.%s' % name, {}, {}, ['Command']), 'Command')()
@ -142,11 +192,13 @@ def load_commands(command_dir, exclude=[]):
except OSError, e:
return {}
class ConsoleUI(component.Component):
def __init__(self, args=None):
def __init__(self, args=None, cmds = None, daemon = None):
component.Component.__init__(self, "ConsoleUI", 2)
self.batch_write = False
# keep track of events for the log view
self.events = []
try:
locale.setlocale(locale.LC_ALL, '')
@ -155,8 +207,10 @@ class ConsoleUI(component.Component):
self.encoding = sys.getdefaultencoding()
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)
@ -165,30 +219,18 @@ class ConsoleUI(component.Component):
if args:
args = args[0]
self.interactive = False
# Try to connect to the localhost daemon
def on_connect(result):
def on_started(result):
if not self.interactive:
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.started_deferred.addCallback(on_started)
component.start().addCallback(on_started)
d = client.connect()
d.addCallback(on_connect)
if not cmds:
print "Sorry, couldn't find any commands"
return
else:
self._commands = cmds
from commander import Commander
cmdr = Commander(cmds)
if daemon:
cmdr.exec_args(args,*daemon)
else:
cmdr.exec_args(args,None,None,None,None)
self.coreconfig = CoreConfig()
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
# pass it the function that handles commands
colors.init_colors()
self.screen = screen.Screen(stdscr, self.do_command, self.tab_completer, self.encoding)
self.statusbars = StatusBars()
from modes.connectionmanager import ConnectionManager
self.screen = ConnectionManager(stdscr, self.encoding)
self.eventlog = EventLog()
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
reactor.run()
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
self.torrents = []
def on_session_state(result):
def on_torrents_status(torrents):
for torrent_id, status in torrents.items():
self.torrents.append((torrent_id, status["name"]))
self.started_deferred.callback(True)
if not self.interactive:
self.started_deferred = defer.Deferred()
self.torrents = []
def on_session_state(result):
def on_torrents_status(torrents):
for torrent_id, status in torrents.items():
self.torrents.append((torrent_id, status["name"]))
self.started_deferred.callback(True)
client.core.get_torrents_status({"id": result}, ["name"]).addCallback(on_torrents_status)
client.core.get_session_state().addCallback(on_session_state)
# Register some event handlers to keep the torrent list up-to-date
client.register_event_handler("TorrentAddedEvent", self.on_torrent_added_event)
client.register_event_handler("TorrentRemovedEvent", self.on_torrent_removed_event)
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
client.core.get_torrents_status({"id": result}, ["name"]).addCallback(on_torrents_status)
client.core.get_session_state().addCallback(on_session_state)
def match_torrent(self, string):
"""
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
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):
for index, (tid, name) in enumerate(self.torrents):
if event.torrent_id == tid:
del self.torrents[index]
def get_torrent_name(self, torrent_id):
if self.interactive and hasattr(self.screen,"get_torrent_name"):
return self.screen.get_torrent_name(torrent_id)
for tid, name in self.torrents:
if torrent_id == tid:
return name
return None
def set_batch_write(self, batch):
# 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):
component.stop()
def write(self, s):
if self.interactive:
self.events.append(s)
else:
print colors.strip_colors(s)

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -41,7 +41,6 @@ class StatusBars(component.Component):
def __init__(self):
component.Component.__init__(self, "StatusBars", 2, depend=["CoreConfig"])
self.config = component.get("CoreConfig")
self.screen = component.get("ConsoleUI").screen
# Hold some values we get from the core
self.connections = 0
@ -49,6 +48,10 @@ class StatusBars(component.Component):
self.upload = ""
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):
self.update()
@ -77,30 +80,28 @@ class StatusBars(component.Component):
def update_statusbars(self):
# 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():
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:
self.screen.topbar += "Not Connected"
self.topbar += "Not Connected"
# 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:
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:
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:
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"]:
self.screen.bottombar += " DHT: %s" % self.dht
self.screen.refresh()
self.bottombar += " DHT: %s" % self.dht