diff --git a/deluge/ui/console/colors.py b/deluge/ui/console/colors.py index 304864313..f4ab34471 100644 --- a/deluge/ui/console/colors.py +++ b/deluge/ui/console/colors.py @@ -50,6 +50,15 @@ schemes = { "event": ("magenta", "black", "bold") } +# Colors for various torrent states +state_color = { + "Seeding": "{{blue,black,bold}}", + "Downloading": "{{green,black,bold}}", + "Paused": "{{white,black}}", + "Checking": "{{green,black}}", + "Queued": "{{yellow,black}}", + "Error": "{{red,black,bold}}" +} def init_colors(): # Create the color_pairs dict @@ -65,6 +74,19 @@ def init_colors(): class BadColorString(Exception): pass +def get_line_length(line): + """ + Returns the string length without the color formatting. + + """ + if line.count("{{") != line.count ("}}"): + raise BadColorString("Number of {{ does not equal number of }}") + + while line.find("{{") != -1: + line = line[:line.find("{{")] + line[line.find("}}") + 2:] + + return len(line) + def parse_color_string(s): """ Parses a string and returns a list of 2-tuples (color, string). @@ -72,6 +94,9 @@ def parse_color_string(s): :param s:, string to parse """ + if s.count("{{") != s.count ("}}"): + raise BadColorString("Number of {{ does not equal number of }}") + ret = [] # Keep track of where the strings col_index = 0 diff --git a/deluge/ui/console/commands/add.py b/deluge/ui/console/commands/add.py index 4da35f7ad..f1496f049 100644 --- a/deluge/ui/console/commands/add.py +++ b/deluge/ui/console/commands/add.py @@ -22,10 +22,11 @@ # 51 Franklin Street, Fifth Floor # Boston, MA 02110-1301, USA. # -from deluge.ui.console.main import BaseCommand, match_torrents -from deluge.ui.console import mapping +from deluge.ui.console.main import BaseCommand import deluge.ui.console.colors as colors from deluge.ui.client import client +import deluge.component as component + from optparse import make_option import os import base64 @@ -40,18 +41,20 @@ class Command(BaseCommand): usage = "Usage: add [-p ] [ ...]" def handle(self, *args, **options): + self.console = component.get("ConsoleUI") + t_options = {} if options["path"]: t_options["download_location"] = options["path"] for arg in args: - self.write("{{info}}Attempting to add torrent: %s" % arg) + self.console.write("{{info}}Attempting to add torrent: %s" % arg) filename = os.path.split(arg)[-1] filedump = base64.encodestring(open(arg).read()) def on_success(result): - self.write("{{success}}Torrent added!") + self.console.write("{{success}}Torrent added!") def on_fail(result): - self.write("{{error}}Torrent was not added! %s" % result) + self.console.write("{{error}}Torrent was not added! %s" % result) client.core.add_torrent_file(filename, filedump, t_options).addCallback(on_success).addErrback(on_fail) diff --git a/deluge/ui/console/commands/connect.py b/deluge/ui/console/commands/connect.py index 5b9e0f20f..f5bc684c4 100644 --- a/deluge/ui/console/commands/connect.py +++ b/deluge/ui/console/commands/connect.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # # connect.py # @@ -25,17 +24,20 @@ from deluge.ui.console.main import BaseCommand import deluge.ui.console.colors as colors from deluge.ui.client import client +import deluge.component as component class Command(BaseCommand): """Connect to a new deluge server.""" - def handle(self, host='localhost', port='58846', username="", password="", **options): + def handle(self, host="", port="58846", username="", password="", **options): + self.console = component.get("ConsoleUI") + port = int(port) d = client.connect(host, port, username, password) def on_connect(result): - print templates.SUCCESS('Connected to %s:%d!' % (host, port)) + self.console.write("{{success}}Connected to %s:%s!" % (host, port)) def on_connect_fail(result): - print templates.ERROR("Failed to connect to %s:%d!" % (host, port)) + self.console.write("{{error}}Failed to connect to %s:%s!" % (host, port)) d.addCallback(on_connect) d.addErrback(on_connect_fail) diff --git a/deluge/ui/console/commands/halt.py b/deluge/ui/console/commands/halt.py index 8e87a29d9..ed4242685 100644 --- a/deluge/ui/console/commands/halt.py +++ b/deluge/ui/console/commands/halt.py @@ -1,8 +1,8 @@ -#!/usr/bin/env python # # halt.py # # Copyright (C) 2008-2009 Ido Abramovich +# Copyright (C) 2009 Andrew Resch # # Deluge is free software. # @@ -22,12 +22,20 @@ # 51 Franklin Street, Fifth Floor # Boston, MA 02110-1301, USA. # -from deluge.ui.console.main import BaseCommand, match_torrents -from deluge.ui.console import mapping +from deluge.ui.console.main import BaseCommand import deluge.ui.console.colors as colors from deluge.ui.client import client +import deluge.component as component class Command(BaseCommand): "Shutdown the deluge server." def handle(self, **options): - client.daemon.shutdown(None) + self.console = component.get("ConsoleUI") + + def on_shutdown(result): + self.write("{{success}}Daemon was shutdown") + + def on_shutdown_fail(reason): + self.write("{{error}}Unable to shutdown daemon: %s" % reason) + + client.daemon.shutdown().addCallback(on_shutdown).addErrback(on_shutdown_fail) diff --git a/deluge/ui/console/commands/help.py b/deluge/ui/console/commands/help.py index 0542fb383..dc8ba1db9 100644 --- a/deluge/ui/console/commands/help.py +++ b/deluge/ui/console/commands/help.py @@ -1,8 +1,8 @@ -#!/usr/bin/env python # # help.py # # Copyright (C) 2008-2009 Ido Abramovich +# Copyright (C) 2009 Andrew Resch # # Deluge is free software. # @@ -22,46 +22,44 @@ # 51 Franklin Street, Fifth Floor # Boston, MA 02110-1301, USA. # -from deluge.ui.console import UI_PATH -from deluge.ui.console.main import BaseCommand, load_commands +from deluge.ui.console.main import BaseCommand import deluge.ui.console.colors as colors -#from deluge.ui.console.colors import templates -import os +import deluge.component as component class Command(BaseCommand): """displays help on other commands""" usage = "Usage: help [command]" - def __init__(self): - BaseCommand.__init__(self) +# def __init__(self): +# BaseCommand.__init__(self) # get a list of commands, exclude 'help' so we won't run into a recursive loop. - self._commands = load_commands(os.path.join(UI_PATH,'commands'), None, exclude=['help']) - self._commands['help'] = self + #self._commands = load_commands(os.path.join(UI_PATH,'commands'), None, exclude=['help']) + +# self._commands['help'] = self def handle(self, *args, **options): + self.console = component.get("ConsoleUI") + self._commands = self.console._commands if args: if len(args) > 1: #print usage - self.write(usage) + self.console.write(usage) return try: cmd = self._commands[args[0]] except KeyError: #print templates.ERROR('unknown command %r' % args[0]) - self.write("{{error}}Unknown command %r" % args[0]) + self.console.write("{{error}}Unknown command %r" % args[0]) return try: parser = cmd.create_parser() - self.write(parser.format_help()) + self.console.write(parser.format_help()) except AttributeError, e: - self.write(cmd.__doc__ or 'No help for this command') + self.console.write(cmd.__doc__ or 'No help for this command') else: max_length = max( len(k) for k in self._commands) for cmd in sorted(self._commands): - self.write("{{info}}" + cmd + "{{input}} - " + self._commands[cmd].__doc__ or '') - self.write("") - self.write('For help on a specific command, use " --help"') - - def complete(self, text, *args): - return [ x for x in self._commands.keys() if x.startswith(text) ] + self.console.write("{{info}}" + cmd + "{{input}} - " + self._commands[cmd].__doc__ or '') + self.console.write("") + self.console.write('For help on a specific command, use " --help"') diff --git a/deluge/ui/console/commands/info.py b/deluge/ui/console/commands/info.py index ec26574fe..24c800156 100644 --- a/deluge/ui/console/commands/info.py +++ b/deluge/ui/console/commands/info.py @@ -1,8 +1,8 @@ -#!/usr/bin/env python # # info.py # # Copyright (C) 2008-2009 Ido Abramovich +# Copyright (C) 2009 Andrew Resch # # Deluge is free software. # @@ -23,13 +23,13 @@ # Boston, MA 02110-1301, USA. # -from deluge.ui.console.main import BaseCommand, match_torrents -from deluge.ui.console import mapping +from optparse import make_option + +from deluge.ui.console.main import BaseCommand import deluge.ui.console.colors as colors -#from deluge.ui.console.colors import templates from deluge.ui.client import client import deluge.common as common -from optparse import make_option +import deluge.component as component status_keys = ["state", "save_path", @@ -73,25 +73,58 @@ class Command(BaseCommand): def handle(self, *args, **options): - def on_to_ids(result): - def on_match_torrents(torrents): - for torrent in torrents: - self.show_info(torrent, options.get("verbose")) + self.console = component.get("ConsoleUI") + # Compile a list of torrent_ids to request the status of + torrent_ids = [] + for arg in args: + torrent_ids.extend(self.console.match_torrent(arg)) - match_torrents(result).addCallback(on_match_torrents) - mapping.to_ids(args).addCallback(on_to_ids) + def on_torrents_status(status): + # Print out the information for each torrent + for key, value in status.items(): + self.show_info(key, value, options["verbose"]) -# def complete(self, text, *args): -# torrents = match_torrents() -# names = mapping.get_names(torrents) -# return [ x[1] for x in names if x[1].startswith(text) ] + def on_torrents_status_fail(reason): + self.console.write("{{error}}Error getting torrent info: %s" % reason) - def show_info(self, torrent, verbose): + d = client.core.get_torrents_status({"id": torrent_ids}, status_keys) + d.addCallback(on_torrents_status) + d.addErrback(on_torrents_status_fail) + + def show_info(self, torrent_id, status, verbose=False): + """ + Writes out the torrents information to the screen. + + :param torrent_id: str, the torrent_id + :param status: dict, the torrents status, this should have the same keys + as status_keys + :param verbose: bool, if true, we print out more information about the + the torrent + """ + self.console.write(" ") + self.console.write("{{info}}Name: {{input}}%s" % (status["name"])) + self.console.write("{{info}}ID: {{input}}%s" % (torrent_id)) + s = "{{info}}State: %s%s" % (colors.state_color[status["state"]], status["state"]) + # Only show speed if active + if status["state"] in ("Seeding", "Downloading"): + if status["state"] != "Seeding": + s += " {{info}}Down Speed: {{input}}%s" % common.fspeed(status["download_payload_rate"]) + s += " {{info}}Up Speed: {{input}}%s" % common.fspeed(status["upload_payload_rate"]) + + if common.ftime(status["eta"]): + s += " {{info}}ETA: {{input}}%s" % common.ftime(status["eta"]) + + self.console.write(s) + + s = "{{info}}Seeds: {{input}}%s (%s)" % (status["num_seeds"], status["total_seeds"]) + s += " {{info}}Peers: {{input}}%s (%s)" % (status["num_peers"], status["total_peers"]) + s += " {{info}}Ratio: {{input}}%.3f" % status["ratio"] + s += " {{info}}Availibility: {{input}}%.2f" % status["distributed_copies"] + self.console.write(s) + + +""" def __show_info(self, torrent, verbose): def _got_torrent_status(state): - print templates.info_general('ID', torrent) - print templates.info_general('Name', state['name']) - #self._mapping[state['name']] = torrent # update mapping - print templates.info_general('Path', state['save_path']) if verbose or not state['is_seed']: print templates.info_transfers("Completed", common.fsize(state['total_done']) + "/" + common.fsize(state['total_size'])) @@ -129,3 +162,4 @@ class Command(BaseCommand): str(common.fspeed(peer['up_speed'])), str(common.fspeed(peer['down_speed']))) print "" client.core.get_torrent_status(torrent, status_keys).addCallback(_got_torrent_status) +""" diff --git a/deluge/ui/console/commands/pause.py b/deluge/ui/console/commands/pause.py index a7c49974b..0e9dd7308 100644 --- a/deluge/ui/console/commands/pause.py +++ b/deluge/ui/console/commands/pause.py @@ -1,8 +1,8 @@ -#!/usr/bin/env python # # pause.py # # Copyright (C) 2008-2009 Ido Abramovich +# Copyright (C) 2009 Andrew Resch # # Deluge is free software. # @@ -22,30 +22,27 @@ # 51 Franklin Street, Fifth Floor # Boston, MA 02110-1301, USA. # -from deluge.ui.console.main import BaseCommand, match_torrents -from deluge.ui.console import mapping +from deluge.ui.console.main import BaseCommand from deluge.ui.client import client import deluge.ui.console.colors as colors +import deluge.component as component class Command(BaseCommand): """Pause a torrent""" - usage = "Usage: pause [ all | [ ...] ]" + usage = "Usage: pause [ * | [ ...] ]" def handle(self, *args, **options): - if len(args) == 0: - print self.usage - return - if len(args) == 1 and args[0] == 'all': - args = tuple() # empty tuple means everything - try: - args = mapping.to_ids(args) - torrents = match_torrents(args) - client.pause_torrent(torrents) - except Exception, msg: - print templates.ERROR(str(msg)) - else: - print templates.SUCCESS('torrent%s successfully paused' % ('s' if len(args) > 1 else '')) + self.console = component.get("ConsoleUI") - def complete(self, text, *args): - torrents = match_torrents() - names = mapping.get_names(torrents) - return [ x[1] for x in names if x[1].startswith(text) ] + if len(args) == 0: + self.console.write(self.usage) + return + if len(args) > 0 and args[0].lower() == '*': + client.core.pause_all_torrents() + return + + torrent_ids = [] + for arg in args: + torrent_ids.extend(self.console.match_torrent(arg)) + + if torrent_ids: + client.core.pause_torrent(torrent_ids) diff --git a/deluge/ui/console/commands/quit.py b/deluge/ui/console/commands/quit.py index 7053604d3..6cdd5c603 100644 --- a/deluge/ui/console/commands/quit.py +++ b/deluge/ui/console/commands/quit.py @@ -34,3 +34,5 @@ class Command(BaseCommand): def on_disconnect(result): reactor.stop() client.disconnect().addCallback(on_disconnect) + else: + reactor.stop() diff --git a/deluge/ui/console/commands/resume.py b/deluge/ui/console/commands/resume.py index 764107ef3..119930220 100644 --- a/deluge/ui/console/commands/resume.py +++ b/deluge/ui/console/commands/resume.py @@ -1,8 +1,8 @@ -#!/usr/bin/env python # # resume.py # # Copyright (C) 2008-2009 Ido Abramovich +# Copyright (C) 2009 Andrew Resch # # Deluge is free software. # @@ -22,30 +22,27 @@ # 51 Franklin Street, Fifth Floor # Boston, MA 02110-1301, USA. # -from deluge.ui.console.main import BaseCommand, match_torrents -from deluge.ui.console import mapping +from deluge.ui.console.main import BaseCommand from deluge.ui.client import client import deluge.ui.console.colors as colors +import deluge.component as component class Command(BaseCommand): """Resume a torrent""" - usage = "Usage: resume [ all | [ ...] ]" + usage = "Usage: resume [ * | [ ...] ]" def handle(self, *args, **options): - if len(args) == 0: - print self.usage - return - if len(args) == 1 and args[0] == 'all': - args = tuple() # empty tuple means everything - try: - args = mapping.to_ids(args) - torrents = match_torrents(args) - client.resume_torrent(torrents) - except Exception, msg: - print templates.ERROR(str(msg)) - else: - print templates.SUCCESS('torrent%s successfully resumed' % ('s' if len(args) > 1 else '')) + self.console = component.get("ConsoleUI") - def complete(self, text, *args): - torrents = match_torrents() - names = mapping.get_names(torrents) - return [ x[1] for x in names if x[1].startswith(text) ] + if len(args) == 0: + self.console.write(self.usage) + return + if len(args) > 0 and args[0] == '*': + client.core.resume_all_torrents() + return + + torrent_ids = [] + for arg in args: + torrent_ids.extend(self.console.match_torrent(arg)) + + if torrent_ids: + client.core.resume_torrent(torrent_ids) diff --git a/deluge/ui/console/commands/rm.py b/deluge/ui/console/commands/rm.py index b37d0c824..d5a0a2df9 100644 --- a/deluge/ui/console/commands/rm.py +++ b/deluge/ui/console/commands/rm.py @@ -1,8 +1,8 @@ -#!/usr/bin/env python # # rm.py # # Copyright (C) 2008-2009 Ido Abramovich +# Copyright (C) 2009 Andrew Resch # # Deluge is free software. # @@ -22,12 +22,13 @@ # 51 Franklin Street, Fifth Floor # Boston, MA 02110-1301, USA. # -from deluge.ui.console.main import BaseCommand, match_torrents -from deluge.ui.console import mapping +from deluge.ui.console.main import BaseCommand import deluge.ui.console.colors as colors from deluge.ui.client import client +import deluge.component as component + from optparse import make_option -import os + class Command(BaseCommand): """Remove a torrent""" @@ -40,14 +41,12 @@ class Command(BaseCommand): ) def handle(self, *args, **options): - try: - args = mapping.to_ids(args) - torrents = match_torrents(args) - client.remove_torrent(torrents, options['remove_data']) - except Exception, msg: - print template.ERROR(str(msg)) + self.console = component.get("ConsoleUI") + if len(args) == 0: + self.console.write(usage) - def complete(self, text, *args): - torrents = match_torrents() - names = mapping.get_names(torrents) - return [ x[1] for x in names if x[1].startswith(text) ] + torrent_ids = [] + for arg in args: + torrent_ids.extend(self.console.match_torrent(arg)) + + client.core.remove_torrent(torrent_ids, options['remove_data']) diff --git a/deluge/ui/console/eventlog.py b/deluge/ui/console/eventlog.py index daafd9d7b..8f6dac0e2 100644 --- a/deluge/ui/console/eventlog.py +++ b/deluge/ui/console/eventlog.py @@ -25,6 +25,7 @@ import deluge.component as component import deluge.common +import colors from deluge.ui.client import client from deluge.log import LOG as log @@ -58,16 +59,8 @@ class EventLog(component.Component): def on_torrent_state_changed_event(self, torrent_id, state): log.debug("on_torrent_state_changed_event!") # Modify the state string color - state_color = { - "Seeding": "{{blue,black,bold}}", - "Downloading": "{{green,black,bold}}", - "Paused": "{{white,black}}", - "Checking": "{{green,black}}", - "Queued": "{{yellow,black}}", - "Error": "{{red,black,bold}}" - } - if state in state_color: - state = state_color[state] + state + if state in colors.state_color: + state = colors.state_color[state] + state self.console.write("{{event}}* TorrentStateChanged: %s {{info}}%s (%s)" % (state, self.console.get_torrent_name(torrent_id), torrent_id)) diff --git a/deluge/ui/console/main.py b/deluge/ui/console/main.py index 5517246c6..99599adf3 100644 --- a/deluge/ui/console/main.py +++ b/deluge/ui/console/main.py @@ -25,6 +25,10 @@ import os, sys import optparse +import shlex + +from twisted.internet import defer, reactor + from deluge.ui.console import UI_PATH import deluge.component as component from deluge.ui.client import client @@ -32,14 +36,14 @@ import deluge.common from deluge.ui.coreconfig import CoreConfig from deluge.ui.console.statusbars import StatusBars from deluge.ui.console.eventlog import EventLog - -from twisted.internet import defer, reactor -import shlex -import screen -import colors - +import deluge.ui.console.screen as screen +import deluge.ui.console.colors as colors from deluge.log import LOG as log +# XXX: Remove when the commands are all fixed up +def match_torrents(a=[]): + pass + class OptionParser(optparse.OptionParser): """subclass from optparse.OptionParser so exit() won't exit.""" def exit(self, status=0, msg=None): @@ -62,7 +66,6 @@ class BaseCommand(object): option_list = tuple() aliases = [] - def complete(self, text, *args): return [] def handle(self, *args, **options): @@ -85,24 +88,7 @@ class BaseCommand(object): epilog = self.epilog, option_list = self.option_list) - -def match_torrents(array=[]): - # Make sure we don't have any duplicates - array = set(array) - # We return this defer and it will be fired once we received the session - # state and intersect the data. - d = defer.Deferred() - - def _got_session_state(tors): - if not array: - d.callback(tors) - d.callback(list(tors.intersection(array))) - - client.core.get_session_state().addCallback(_got_session_state) - - return d - -def load_commands(command_dir, write_func, exclude=[]): +def load_commands(command_dir, exclude=[]): def get_command(name): return getattr(__import__('deluge.ui.console.commands.%s' % name, {}, {}, ['Command']), 'Command')() @@ -112,8 +98,6 @@ def load_commands(command_dir, write_func, exclude=[]): if filename.split('.')[0] in exclude or filename.startswith('_') or not filename.endswith('.py'): continue cmd = get_command(filename[:-3]) - # Hack to give the commands a write function - cmd.write = write_func aliases = [ filename[:-3] ] aliases.extend(cmd.aliases) for a in aliases: @@ -126,7 +110,7 @@ class ConsoleUI(component.Component): def __init__(self, args=None): component.Component.__init__(self, "ConsoleUI", 2) # Load all the commands - self._commands = load_commands(os.path.join(UI_PATH, 'commands'), self.write) + self._commands = load_commands(os.path.join(UI_PATH, 'commands')) # Try to connect to the localhost daemon def on_connect(result): @@ -236,10 +220,11 @@ class ConsoleUI(component.Component): if not getattr(options, '_exit', False): try: self._commands[cmd].handle(*args, **options.__dict__) - except StopIteration, e: - raise except Exception, e: self.write("{{error}}" + str(e)) + log.exception(e) + import traceback + self.write("%s" % traceback.format_exc()) def tab_completer(self, line, cursor, second_hit): """ @@ -330,9 +315,27 @@ class ConsoleUI(component.Component): return None + def match_torrent(self, string): + """ + Returns a list of torrent_id matches for the string. It will search both + torrent_ids and torrent names, but will only return torrent_ids. + + :param string: str, the string to match on + + :returns: list of matching torrent_ids. Will return an empty list if + no matches are found. + + """ + ret = [] + for tid, name in self.torrents: + if tid.startswith(string) or name.startswith(string): + ret.append(tid) + + return ret + def on_torrent_added_event(self, torrent_id): def on_torrent_status(status): - self.torrents.append(torrent_id, status["name"]) + self.torrents.append((torrent_id, status["name"])) client.get_torrent_status(torrent_id, ["name"]).addCallback(on_torrent_status) def on_torrent_removed_event(self, torrent_id): diff --git a/deluge/ui/console/screen.py b/deluge/ui/console/screen.py index 6337ed59e..3bbc97a4d 100644 --- a/deluge/ui/console/screen.py +++ b/deluge/ui/console/screen.py @@ -116,15 +116,6 @@ class Screen(CursesStdIO): """ log.debug("adding line: %s", text) - def get_line_length(line): - """ - Returns the string length without the color formatting. - - """ - while line.find("{{") != -1: - line = line[:line.find("{{")] + line[line.find("}}") + 2:] - - return len(line) def get_line_chunks(line): """ @@ -155,7 +146,12 @@ class Screen(CursesStdIO): for line in text.splitlines(): # We need to check for line lengths here and split as necessary - line_length = get_line_length(line) + try: + line_length = colors.get_line_length(line) + except colors.BadColorString: + log.error("Passed a bad colored string..") + line_length = len(line) + if line_length >= (self.cols - 1): s = "" # The length of the text without the color tags @@ -198,7 +194,12 @@ class Screen(CursesStdIO): """ col = 0 - parsed = colors.parse_color_string(string) + try: + parsed = colors.parse_color_string(string) + except colors.BadColorString: + log.error("Cannot add bad color string: %s", string) + return + for index, (color, s) in enumerate(parsed): if index + 1 == len(parsed): # This is the last string so lets append some " " to it