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.erase() 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 (up & down arrows to navigate, esc to cancel)",close_cb=self.__do_add) self.popup.add_text_input("Hostname:","hostname") self.popup.add_text_input("Port:","port") self.popup.add_text_input("Username:"******"username") self.popup.add_text_input("Password:"******"password") 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.erase() 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 on_resize(self, *args): BaseMode.on_resize_norefresh(self, *args) if self.popup: self.popup.handle_resize() self.stdscr.erase() self.refresh() 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
class TorrentDetail(BaseMode, component.Component): def __init__(self, alltorrentmode, torrentid, stdscr, console_config, encoding=None): self.console_config = console_config 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", "total_wanted", "tracker_host", "owner"] 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.full_names = None self.column_string = "" self.files_sep = None self.marked = {} BaseMode.__init__(self, stdscr, encoding) component.Component.__init__(self, "TorrentDetail", 1, depend=["SessionProxy"]) self.column_names = ["Filename", "Size", "Progress", "Priority"] self.__update_columns() component.start(["TorrentDetail"]) self._listing_start = self.rows // 2 self._listing_space = self._listing_start - self._listing_start client.register_event_handler("TorrentFileRenamedEvent", self._on_torrentfilerenamed_event) client.register_event_handler("TorrentFolderRenamedEvent", self._on_torrentfolderrenamed_event) client.register_event_handler("TorrentRemovedEvent", self._on_torrentremoved_event) 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 state.get("files"): self.full_names = dict([ (x['index'], x['path']) for x in state["files"]]) need_prio_update = False if not self.file_list: # don't keep getting the files once we've got them once if state.get("files"): 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") else: self.files_sep = "{!green,black,bold,underline!}%s"%(("Files (File list unknown)").center(self.cols)) need_prio_update = True self.__fill_progress(self.file_list,state["file_progress"]) for i,prio in enumerate(state["file_priorities"]): if self.file_dict[i][6] != prio: need_prio_update = True self.file_dict[i][6] = prio if need_prio_update: self.__fill_prio(self.file_list) 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): if not progs: return 0 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 __fill_prio(self,fs): for f in fs: if f[3]: # dir, so fill in children and compute our prio self.__fill_prio(f[3]) s = set([e[6] for e in f[3]]) # pull out all child prios and turn into a set if len(s) > 1: f[6] = -2 # mixed else: f[6] = s.pop() 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 _on_torrentremoved_event(self, torrent_id): if torrent_id == self.torrentid: self.back_to_overview() def _on_torrentfilerenamed_event(self, torrent_id, index, new_name): if torrent_id == self.torrentid: self.file_dict[index][0] = new_name.split("/")[-1] component.get("SessionProxy").get_torrent_status(self.torrentid, self._status_keys).addCallback(self.set_state) def _on_torrentfolderrenamed_event(self, torrent_id, old_folder, new_folder): if torrent_id == self.torrentid: fe = None fl = None for i in old_folder.strip("/").split("/"): if not fl: fe = fl = self.file_list s = filter(lambda x: x[0].strip("/") == i, fl)[0] fe = s fl = s[3] fe[0] = new_folder.strip("/").rpartition("/")[-1] #self.__get_file_by_name(old_folder, self.file_list)[0] = new_folder.strip("/") component.get("SessionProxy").get_torrent_status(self.torrentid, self._status_keys).addCallback(self.set_state) def draw_files(self,files,depth,off,idx): color_selected = "blue" color_partially_selected = "magenta" color_highlighted = "white" for fl in files: #from sys import stderr #print >> stderr, fl[6] # 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 # default color values fg = "white" bg = "black" attr = "" if fl[6] == -2: priority = -1 #Mixed elif fl[6] == 0: priority = 0 #Do Not Download fg = "red" elif fl[6] == 1: priority = 1 #Normal elif fl[6] <= 6: priority = 2 #High fg = "yellow" elif fl[6] == 7: priority = 3 #Highest fg = "green" if idx >= self.file_off: # set fg/bg colors based on whether the file is selected/marked or not if fl[1] in self.marked: bg = color_selected if fl[3]: if self.marked[fl[1]] < self.__get_contained_files_count(file_list=fl[3]): bg = color_partially_selected attr = "bold" if idx == self.current_file_idx: self.current_file = fl bg = color_highlighted if fl[1] in self.marked: fg = color_selected if fl[3]: if self.marked[fl[1]] < self.__get_contained_files_count(file_list = fl[3]): fg = color_partially_selected else: if fg == "white": fg = "black" attr = "bold" if attr: color_string = "{!%s,%s,%s!}"%(fg, bg, attr) else: 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 __get_file_list_length(self, file_list=None): """ Counts length of the displayed file list. """ if file_list == None: file_list = self.file_list length = 0 if file_list: for element in file_list: length += 1 if element[3] and element[4]: length += self.__get_file_list_length(element[3]) return length def __get_contained_files_count(self, file_list=None, idx = None): length = 0 if file_list == None: file_list = self.file_list if idx != None: for element in file_list: if element[1] == idx: return self.__get_contained_files_count(file_list = element[3]) elif element[3]: c = self.__get_contained_files_count(file_list = element[3], idx=idx) if c > 0: return c else: for element in file_list: length += 1 if element[3]: length -= 1 length += self.__get_contained_files_count(element[3]) return length def on_resize(self, *args): BaseMode.on_resize_norefresh(self, *args) #Always refresh Legacy(it will also refresh AllTorrents), otherwise it will bug deluge out legacy = component.get("LegacyUI") legacy.on_resize(*args) self.__update_columns() if self.popup: self.popup.handle_resize() self._listing_start = self.rows / 2 self.refresh() def render_header(self, off): status = self.torrent_state up_color = colors.state_color["Seeding"] down_color = colors.state_color["Downloading"] #Name s = "{!info!}Name: {!input!}%s" % status["name"] self.add_string(off, s); off += 1 #Print DL info and ETA if status["download_payload_rate"] > 0: s = "%sDownloading: {!input!}" % down_color else: s = "{!info!}Downloaded: {!input!}" s+= common.fsize(status["all_time_download"]) if status["progress"] != 100.0: s+= "/%s" % common.fsize(status["total_wanted"]) if status["download_payload_rate"] > 0: s+= " {!yellow!}@ %s%s" % (down_color, common.fsize(status["download_payload_rate"])) s+= "{!info!} ETA: {!input!}%s" % format_utils.format_time(status["eta"]) self.add_string(off, s); off += 1 #Print UL info and ratio if status["upload_payload_rate"] > 0: s = "%sUploading: {!input!}" % up_color else: s = "{!info!}Uploaded: {!input!}" s+= common.fsize(status["total_uploaded"]) if status["upload_payload_rate"] > 0: s+= " {!yellow!}@ %s%s" % (up_color, common.fsize(status["upload_payload_rate"])) ratio_str = format_utils.format_float(status["ratio"]) if ratio_str == "-": ratio_str = "inf" s+= " {!info!}Ratio: {!input!}%s" % ratio_str self.add_string(off, s); off += 1 #Seeder/leecher info s = "{!info!}Seeders:{!green!} %s {!input!}(%s)" % (status["num_seeds"], status["total_seeds"]) self.add_string(off, s); off += 1 s = "{!info!}Leechers:{!red!} %s {!input!}(%s)" % (status["num_peers"], status["total_peers"]) self.add_string(off, s); off += 1 #Tracker if status["message"] == "OK": color = "{!green!}" else: color = "{!red!}" s = "{!info!}Tracker: {!magenta!}%s{!input!} says \"%s%s{!input!}\"" % (status["tracker_host"], color, status["message"]) self.add_string(off, s); off += 1 #Pieces and availability s = "{!info!}Pieces: {!yellow!}%s {!input!}x {!yellow!}%s" % (status["num_pieces"], common.fsize(status["piece_length"])) if status["distributed_copies"]: s+= " {!info!}Availability: {!input!}%s" % format_utils.format_float(status["distributed_copies"]) self.add_string(off, s); off += 1 #Time added s = "{!info!}Added: {!input!}%s" % common.fdate(status["time_added"]) self.add_string(off, s); off += 1 #Time active s = "{!info!}Time active: {!input!}%s" % ( common.ftime(status["active_time"]) ) if status["seeding_time"]: s+= ", {!cyan!}%s{!input!} seeding" % ( common.ftime(status["seeding_time"]) ) self.add_string(off, s); off += 1 #Save Path s = "{!info!}Save path: {!input!}%s" % status["save_path"] self.add_string(off, s); off += 1 #Owner if status["owner"]: s = "{!info!}Owner: {!input!}%s" % status["owner"] return off 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.erase() self.add_string(0,self.statusbars.topbar) #This will quite likely fail when switching modes try: rf = format_utils.remove_formatting string = self.statusbars.bottombar hstr = "Press {!magenta,blue,bold!}[h]{!status!} for help" string += " " * ( self.cols - len(rf(string)) - len(rf(hstr))) + hstr self.add_string(self.rows - 1, string) except: pass off = 1 if self.torrent_state: off = self.render_header(off) else: self.add_string(1, "Waiting for torrent state") off += 1 if self.files_sep: self.add_string(off, self.files_sep) off += 1 self._listing_start = off self._listing_space = self.rows - self._listing_start 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) if component.get("ConsoleUI").screen != self: return self.stdscr.noutrefresh() if self.popup: self.popup.refresh() curses.doupdate() def expcol_cur_file(self): """ Expand or collapse current file """ self.current_file[4] = not self.current_file[4] self.refresh() def file_list_down(self, rows=1): maxlen = self.__get_file_list_length() - 1 self.current_file_idx += rows if self.current_file_idx > maxlen: self.current_file_idx = maxlen if self.current_file_idx > self.file_off + (self._listing_space - 3): self.file_off = self.current_file_idx - (self._listing_space - 3) self.refresh() def file_list_up(self, rows=1): self.current_file_idx = max(0,self.current_file_idx-rows) self.file_off = min(self.file_off,self.current_file_idx) self.refresh() def back_to_overview(self): component.stop(["TorrentDetail"]) component.deregister(self) self.stdscr.erase() component.get("ConsoleUI").set_mode(self.alltorrentmode) self.alltorrentmode._go_top = False self.alltorrentmode.resume() # build list of priorities for all files in the torrent # based on what is currently selected and a selected priority. def build_prio_list(self, files, ret_list, parent_prio, selected_prio): # has a priority been set on my parent (if so, I inherit it) for f in files: #Do not set priorities for the whole dir, just selected contents if f[3]: 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, was_empty): plist = [] self.build_prio_list(self.file_list,plist,-1,data) plist.sort() priorities = [p[1] for p in plist] log.debug("priorities: %s", priorities) client.core.set_torrent_file_priorities(self.torrentid, priorities) if was_empty: self.marked = {} return True # show popup for priority selections def show_priority_popup(self, was_empty): func = lambda idx, data, we=was_empty: self.do_priority(idx, data, we) if self.marked: self.popup = SelectablePopup(self,"Set File Priority", func) self.popup.add_line("_Do Not Download",data=deluge.common.FILE_PRIORITY["Do Not Download"], foreground="red") 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"], foreground="yellow") self.popup.add_line("H_ighest Priority",data=deluge.common.FILE_PRIORITY["Highest Priority"], foreground="green") self.popup._selected = 1 def __mark_unmark(self,idx): """ Selects or unselects file or a catalog(along with contained files) """ fc = self.__get_contained_files_count(idx=idx) if idx not in self.marked: #Not selected, select it self.__mark_tree(self.file_list, idx) elif self.marked[idx] < fc: #Partially selected, unselect all contents self.__unmark_tree(self.file_list, idx) else: #Selected, unselect it self.__unmark_tree(self.file_list, idx) def __mark_tree(self, file_list, idx, mark_all = False): """ Given file_list of TorrentDetail and index of file or folder, recursively selects all files contained as well as marks folders higher in hierarchy as partially selected """ total_marked = 0 for element in file_list: marked = 0 #Select the file if it's the one we want or # if it's inside a directory that got selected if (element[1] == idx) or mark_all: #If it's a folder then select everything inside if element[3]: marked = self.__mark_tree(element[3], idx, True) self.marked[element[1]] = marked else: marked = 1 self.marked[element[1]] = 1 else: #Does not match but the item to be selected might be inside, recurse if element[3]: marked = self.__mark_tree(element[3], idx, False) #Partially select the folder if it contains files that were selected if marked > 0: self.marked[element[1]] = marked else: if element[1] in self.marked: #It's not the element we want but it's marked so count it marked = 1 #Count and then return total amount of files selected in all subdirectories total_marked += marked return total_marked def __get_file_by_num(self, num, file_list, idx = 0): for element in file_list: if idx == num: return element if element[3] and element[4]: i = self.__get_file_by_num(num, element[3], idx + 1) if not isinstance(i, int): return i else: idx = i else: idx += 1 return idx def __get_file_by_name(self, name, file_list, idx = 0): for element in file_list: if element[0].strip("/") == name.strip("/"): return element if element[3] and element[4]: i = self.__get_file_by_name(name, element[3], idx + 1) if not isinstance(i, int): return i else: idx = i else: idx += 1 return idx def __unmark_tree(self, file_list, idx, unmark_all = False): """ Given file_list of TorrentDetail and index of file or folder, recursively deselects all files contained as well as marks folders higher in hierarchy as unselected or partially selected """ total_marked = 0 for element in file_list: marked = 0 #It's either the item we want to select or # a contained item, deselect it if (element[1] == idx) or unmark_all: if element[1] in self.marked: del self.marked[element[1]] #Deselect all contents if it's a catalog if element[3]: self.__unmark_tree(element[3], idx, True) else: #Not file we wanted but it might be inside this folder, recurse inside if element[3]: marked = self.__unmark_tree(element[3], idx, False) #If none of the contents remain selected, unselect this folder as well if marked == 0: if element[1] in self.marked: del self.marked[element[1]] #Otherwise update selection count else: self.marked[element[1]] = marked else: if element[1] in self.marked: marked = 1 #Count and then return selection count so we can update # directories higher up in the hierarchy total_marked += marked return total_marked def _selection_to_file_idx(self, file_list = None, idx = 0, true_idx = 0, closed=False): if not file_list: file_list = self.file_list for element in file_list: if idx == self.current_file_idx: return true_idx #It's a folder if element[3]: i = self._selection_to_file_idx(element[3], idx + 1, true_idx, closed or not element[4]) if isinstance(i, tuple): idx, true_idx = i if element[4]: idx, true_idx = i else: idx += 1 _, true_idx = i else: return i else: if not closed: idx += 1 true_idx += 1 return (idx, true_idx) def _get_full_folder_path(self, num, file_list = None, path = "", idx = 0): if not file_list: file_list = self.file_list for element in file_list: if not element[3]: idx += 1 continue if num == idx: return "%s%s/" % (path, element[0]) if element[4]: i = self._get_full_folder_path(num, element[3], path + element[0] + "/", idx + 1 ) if not isinstance(i, int): return i else: idx = i else: idx += 1 return idx def _do_rename_folder(self, torrent_id, folder, new_folder): client.core.rename_folder(torrent_id, folder, new_folder) def _do_rename_file(self, torrent_id, file_idx, new_filename): if not new_filename: return client.core.rename_files(torrent_id, [(file_idx, new_filename)]) def _show_rename_popup(self): #Perhaps in the future: Renaming multiple files if self.marked: title = "Error (Enter to close)" text = "Sorry, you can't rename multiple files, please clear selection with {!info!}'c'{!normal!} key" self.popup = MessagePopup(self, title, text) else: _file = self.__get_file_by_num(self.current_file_idx, self.file_list) old_filename = _file[0] idx = self._selection_to_file_idx() tid = self.torrentid if _file[3]: def do_rename(result): if not result["new_foldername"]: return old_fname = self._get_full_folder_path(self.current_file_idx) new_fname = "%s/%s/" % (old_fname.strip("/").rpartition("/")[0], result["new_foldername"]) self._do_rename_folder(tid, old_fname, new_fname) popup = InputPopup(self,"Rename folder (Esc to cancel)",close_cb=do_rename) popup.add_text("{!info!}Renaming folder:{!input!}") popup.add_text(" * %s\n" % old_filename) popup.add_text_input("Enter new folder name:", "new_foldername", old_filename.strip("/")) self.popup = popup else: def do_rename(result): fname = "%s/%s" % (self.full_names[idx].rpartition("/")[0], result["new_filename"]) self._do_rename_file(tid, idx, fname) popup = InputPopup(self,"Rename file (Esc to cancel)",close_cb=do_rename) popup.add_text("{!info!}Renaming file:{!input!}") popup.add_text(" * %s\n" % old_filename) popup.add_text_input("Enter new filename:", "new_filename", old_filename) self.popup = popup 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 if not self.torrent_state: # actions below only make sense if there is a torrent state return # Navigate the torrent list if c == curses.KEY_UP: self.file_list_up() elif c == curses.KEY_PPAGE: self.file_list_up(self._listing_space-2) elif c == curses.KEY_HOME: self.file_off = 0 self.current_file_idx = 0 elif c == curses.KEY_DOWN: self.file_list_down() elif c == curses.KEY_NPAGE: self.file_list_down(self._listing_space-2) elif c == curses.KEY_END: self.current_file_idx = self.__get_file_list_length() - 1 self.file_off = self.current_file_idx - (self._listing_space - 3) elif c == curses.KEY_DC: torrent_actions_popup(self, [self.torrentid], action=ACTION.REMOVE) # Enter Key elif c == curses.KEY_ENTER or c == 10: was_empty = (self.marked == {}) self.__mark_tree(self.file_list, self.current_file[1]) self.show_priority_popup(was_empty) # 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]) elif chr(c) == 'r': self._show_rename_popup() elif chr(c) == 'c': self.marked = {} elif chr(c) == 'a': torrent_actions_popup(self,[self.torrentid],details=False) return elif chr(c) == 'o': torrent_actions_popup(self,[self.torrentid],action=ACTION.TORRENT_OPTIONS) return elif chr(c) == 'h': self.popup = MessagePopup(self, "Help", HELP_STR, width_req=0.75) elif chr(c) == 'j': self.file_list_up() if chr(c) == 'k': self.file_list_down() self.refresh()
class AddTorrents(BaseMode, component.Component): def __init__(self, alltorrentmode, stdscr, console_config, encoding=None): self.console_config = console_config self.alltorrentmode = alltorrentmode self.popup = None self.view_offset = 0 self.cursel = 0 self.marked = set() self.last_mark = -1 path = os.path.expanduser(self.console_config["addtorrents_last_path"]) self.path_stack = ["/"] + path.strip("/").split("/") self.path_stack_pos = len(self.path_stack) self.listing_files = [] self.listing_dirs = [] self.raw_rows = [] self.raw_rows_files = [] self.raw_rows_dirs = [] self.formatted_rows = [] self.sort_column = self.console_config["addtorrents_sort_column"] self.reverse_sort = self.console_config["addtorrents_reverse_sort"] BaseMode.__init__(self, stdscr, encoding) self._listing_space = self.rows - 5 self.__refresh_listing() component.Component.__init__(self, "AddTorrents", 1, depend=["SessionProxy"]) component.start(["AddTorrents"]) curses.curs_set(0) self.stdscr.notimeout(0) # component start/update def start(self): pass def update(self): pass def __refresh_listing(self): path = os.path.join(*self.path_stack[: self.path_stack_pos]) listing = os.listdir(path) self.listing_files = [] self.listing_dirs = [] self.raw_rows = [] self.raw_rows_files = [] self.raw_rows_dirs = [] self.formatted_rows = [] for f in listing: if os.path.isdir(os.path.join(path, f)): if self.console_config["addtorrents_show_hidden_folders"]: self.listing_dirs.append(f) elif f[0] != ".": self.listing_dirs.append(f) elif os.path.isfile(os.path.join(path, f)): if self.console_config["addtorrents_show_misc_files"]: self.listing_files.append(f) elif f.endswith(".torrent"): self.listing_files.append(f) for dirname in self.listing_dirs: row = [] full_path = os.path.join(path, dirname) try: size = len(os.listdir(full_path)) except: size = -1 time = os.stat(full_path).st_mtime row = [dirname, size, time, full_path, 1] self.raw_rows.append(row) self.raw_rows_dirs.append(row) # Highlight the directory we came from if self.path_stack_pos < len(self.path_stack): selected = self.path_stack[self.path_stack_pos] ld = sorted(self.listing_dirs, key=lambda n: n.lower()) c = ld.index(selected) self.cursel = c if (self.view_offset + self._listing_space) <= self.cursel: self.view_offset = self.cursel - self._listing_space for filename in self.listing_files: row = [] full_path = os.path.join(path, filename) size = os.stat(full_path).st_size time = os.stat(full_path).st_mtime row = [filename, size, time, full_path, 0] self.raw_rows.append(row) self.raw_rows_files.append(row) self.__sort_rows() def __sort_rows(self): self.console_config["addtorrents_sort_column"] = self.sort_column self.console_config["addtorrents_reverse_sort"] = self.reverse_sort self.console_config.save() self.raw_rows_dirs.sort(key=lambda r: r[0].lower()) if self.sort_column == "name": self.raw_rows_files.sort(key=lambda r: r[0].lower(), reverse=self.reverse_sort) elif self.sort_column == "date": self.raw_rows_files.sort(key=lambda r: r[2], reverse=self.reverse_sort) self.raw_rows = self.raw_rows_dirs + self.raw_rows_files self.__refresh_rows() def __refresh_rows(self): self.formatted_rows = [] for row in self.raw_rows: filename = row[0] size = row[1] time = row[2] if row[4]: if size != -1: size_str = "%i items" % size else: size_str = " unknown" try: filename = filename.decode("utf8") except: pass cols = [filename, size_str, common.fdate(time)] widths = [self.cols - 35, 12, 23] self.formatted_rows.append(format_utils.format_row(cols, widths)) else: # Size of .torrent file itself couldn't matter less so we'll leave it out try: filename = filename.decode("utf8") except: pass cols = [filename, common.fdate(time)] widths = [self.cols - 23, 23] self.formatted_rows.append(format_utils.format_row(cols, widths)) def scroll_list_up(self, distance): self.cursel -= distance if self.cursel < 0: self.cursel = 0 if self.cursel < self.view_offset + 1: self.view_offset = max(self.cursel - 1, 0) def scroll_list_down(self, distance): self.cursel += distance if self.cursel >= len(self.formatted_rows): self.cursel = len(self.formatted_rows) - 1 if (self.view_offset + self._listing_space) <= self.cursel + 1: self.view_offset = self.cursel - self._listing_space + 1 def set_popup(self, pu): self.popup = pu self.refresh() def on_resize(self, *args): BaseMode.on_resize_norefresh(self, *args) # Always refresh Legacy(it will also refresh AllTorrents), otherwise it will bug deluge out legacy = component.get("LegacyUI") legacy.on_resize(*args) if self.popup: self.popup.handle_resize() self._listing_space = self.rows - 5 self.refresh() def refresh(self, lines=None): # Update the status bars self.stdscr.erase() self.add_string(0, self.statusbars.topbar) # This will quite likely fail when switching modes try: rf = format_utils.remove_formatting string = self.statusbars.bottombar hstr = "Press {!magenta,blue,bold!}[h]{!status!} for help" string += " " * (self.cols - len(rf(string)) - len(rf(hstr))) + hstr self.add_string(self.rows - 1, string) except: pass off = 1 # Render breadcrumbs s = "Location: " for i, e in enumerate(self.path_stack): if e == "/": if i == self.path_stack_pos - 1: s += "{!black,red,bold!}root" else: s += "{!red,black,bold!}root" else: if i == self.path_stack_pos - 1: s += "{!black,white,bold!}%s" % e else: s += "{!white,black,bold!}%s" % e if e != len(self.path_stack) - 1: s += "{!white,black!}/" self.add_string(off, s) off += 1 # Render header cols = ["Name", "Contents", "Modification time"] widths = [self.cols - 35, 12, 23] s = "" for i, (c, w) in enumerate(zip(cols, widths)): cn = "" if i == 0: cn = "name" elif i == 2: cn = "date" if cn == self.sort_column: s += "{!black,green,bold!}" + c.ljust(w - 2) if self.reverse_sort: s += "^ " else: s += "v " else: s += "{!green,black,bold!}" + c.ljust(w) self.add_string(off, s) off += 1 # Render files and folders for i, row in enumerate(self.formatted_rows[self.view_offset :]): i += self.view_offset # It's a folder color_string = "" if self.raw_rows[i][4]: if self.raw_rows[i][1] == -1: if i == self.cursel: color_string = "{!black,red,bold!}" else: color_string = "{!red,black!}" else: if i == self.cursel: color_string = "{!black,cyan,bold!}" else: color_string = "{!cyan,black!}" elif i == self.cursel: if self.raw_rows[i][0] in self.marked: color_string = "{!blue,white,bold!}" else: color_string = "{!black,white,bold!}" elif self.raw_rows[i][0] in self.marked: color_string = "{!white,blue,bold!}" self.add_string(off, color_string + row) off += 1 if off > self.rows - 2: break if component.get("ConsoleUI").screen != self: return self.stdscr.noutrefresh() if self.popup: self.popup.refresh() curses.doupdate() def back_to_overview(self): component.stop(["AddTorrents"]) component.deregister(self) self.stdscr.erase() component.get("ConsoleUI").set_mode(self.alltorrentmode) self.alltorrentmode._go_top = False self.alltorrentmode.resume() def _perform_action(self): if self.cursel < len(self.listing_dirs): self._enter_dir() else: s = self.raw_rows[self.cursel][0] if s not in self.marked: self.last_mark = self.cursel self.marked.add(s) self._show_add_dialog() def _enter_dir(self): # Enter currently selected directory dirname = self.raw_rows[self.cursel][0] new_dir = self.path_stack_pos >= len(self.path_stack) new_dir = new_dir or (dirname != self.path_stack[self.path_stack_pos]) if new_dir: self.path_stack = self.path_stack[: self.path_stack_pos] self.path_stack.append(dirname) path = os.path.join(*self.path_stack[: self.path_stack_pos + 1]) if not os.access(path, os.R_OK): self.path_stack = self.path_stack[: self.path_stack_pos] self.popup = MessagePopup(self, "Error", "{!error!}Access denied: %s" % path) self.__refresh_listing() return self.path_stack_pos += 1 self.view_offset = 0 self.cursel = 0 self.last_mark = -1 self.marked = set() self.__refresh_listing() def _show_add_dialog(self): def _do_add(result): ress = {"succ": 0, "fail": 0, "total": len(self.marked), "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("{!input!} * %s: {!error!}%s" % (t_file, msg)) if (ress["succ"] + ress["fail"]) >= ress["total"]: self.alltorrentmode._report_add_status(ress["succ"], ress["fail"], ress["fmsg"]) def success_cb(tid, t_file, ress): if tid: log.debug("added torrent: %s (%s)" % (t_file, tid)) ress["succ"] += 1 if (ress["succ"] + ress["fail"]) >= ress["total"]: self.alltorrentmode._report_add_status(ress["succ"], ress["fail"], ress["fmsg"]) else: fail_cb("Already in session (probably)", t_file, ress) for m in self.marked: filename = m directory = os.path.join(*self.path_stack[: self.path_stack_pos]) path = os.path.join(directory, filename) filedump = base64.encodestring(open(path).read()) t_options = {} if result["location"]: t_options["download_location"] = result["location"] t_options["add_paused"] = result["add_paused"] d = client.core.add_torrent_file(filename, filedump, t_options) d.addCallback(success_cb, filename, ress) d.addErrback(fail_cb, filename, ress) self.console_config["addtorrents_last_path"] = os.path.join(*self.path_stack[: self.path_stack_pos]) self.console_config.save() self.back_to_overview() config = component.get("ConsoleUI").coreconfig dl = config["download_location"] if config["add_paused"]: ap = 0 else: ap = 1 self.popup = InputPopup(self, "Add Torrents (Esc to cancel)", close_cb=_do_add, height_req=17) msg = "Adding torrent files:" for i, m in enumerate(self.marked): name = m msg += "\n * {!input!}%s" % name if i == 5: if i < len(self.marked): msg += "\n {!red!}And %i more" % (len(self.marked) - 5) break self.popup.add_text(msg) self.popup.add_spaces(1) self.popup.add_text_input("Save Location:", "location", dl) self.popup.add_select_input("Add Paused:", "add_paused", ["Yes", "No"], [True, False], ap) def _go_up(self): # Go up in directory hierarchy if self.path_stack_pos > 1: self.path_stack_pos -= 1 self.view_offset = 0 self.cursel = 0 self.last_mark = -1 self.marked = set() self.__refresh_listing() 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 # Navigate the torrent list if c == curses.KEY_UP: self.scroll_list_up(1) elif c == curses.KEY_PPAGE: # self.scroll_list_up(self._listing_space-2) self.scroll_list_up(self.rows // 2) elif c == curses.KEY_HOME: self.scroll_list_up(len(self.formatted_rows)) elif c == curses.KEY_DOWN: self.scroll_list_down(1) elif c == curses.KEY_NPAGE: # self.scroll_list_down(self._listing_space-2) self.scroll_list_down(self.rows // 2) elif c == curses.KEY_END: self.scroll_list_down(len(self.formatted_rows)) elif c == curses.KEY_RIGHT: if self.cursel < len(self.listing_dirs): self._enter_dir() elif c == curses.KEY_LEFT: self._go_up() # Enter Key elif c == curses.KEY_ENTER or c == 10: self._perform_action() # Escape elif c == 27: self.back_to_overview() else: if c > 31 and c < 256: if chr(c) == "h": self.popup = MessagePopup(self, "Help", HELP_STR, width_req=0.75) elif chr(c) == ">": if self.sort_column == "date": self.reverse_sort = not self.reverse_sort else: self.sort_column = "date" self.reverse_sort = True self.__sort_rows() elif chr(c) == "<": if self.sort_column == "name": self.reverse_sort = not self.reverse_sort else: self.sort_column = "name" self.reverse_sort = False self.__sort_rows() elif chr(c) == "m": s = self.raw_rows[self.cursel][0] if s in self.marked: self.marked.remove(s) else: self.marked.add(s) self.last_mark = self.cursel elif chr(c) == "j": self.scroll_list_up(1) elif chr(c) == "k": self.scroll_list_down(1) elif chr(c) == "M": if self.last_mark != -1: if self.last_mark > self.cursel: m = range(self.cursel, self.last_mark) else: m = range(self.last_mark, self.cursel + 1) for i in m: s = self.raw_rows[i][0] self.marked.add(s) elif chr(c) == "c": self.marked.clear() self.refresh()
class TorrentDetail(BaseMode, component.Component): def __init__(self, alltorrentmode, torrentid, stdscr, console_config, encoding=None): self.console_config = console_config 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", "total_wanted", "tracker_host", "owner" ] 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.full_names = None self.column_string = "" self.files_sep = None self.marked = {} BaseMode.__init__(self, stdscr, encoding) component.Component.__init__(self, "TorrentDetail", 1, depend=["SessionProxy"]) self.column_names = ["Filename", "Size", "Progress", "Priority"] self.__update_columns() component.start(["TorrentDetail"]) self._listing_start = self.rows // 2 self._listing_space = self._listing_start - self._listing_start client.register_event_handler("TorrentFileRenamedEvent", self._on_torrentfilerenamed_event) client.register_event_handler("TorrentFolderRenamedEvent", self._on_torrentfolderrenamed_event) client.register_event_handler("TorrentRemovedEvent", self._on_torrentremoved_event) 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 state.get("files"): self.full_names = dict([(x['index'], x['path']) for x in state["files"]]) need_prio_update = False if not self.file_list: # don't keep getting the files once we've got them once if state.get("files"): 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") else: self.files_sep = "{!green,black,bold,underline!}%s" % ( ("Files (File list unknown)").center(self.cols)) need_prio_update = True self.__fill_progress(self.file_list, state["file_progress"]) for i, prio in enumerate(state["file_priorities"]): if self.file_dict[i][6] != prio: need_prio_update = True self.file_dict[i][6] = prio if need_prio_update: self.__fill_prio(self.file_list) 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): if not progs: return 0 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 __fill_prio(self, fs): for f in fs: if f[3]: # dir, so fill in children and compute our prio self.__fill_prio(f[3]) s = set([e[6] for e in f[3] ]) # pull out all child prios and turn into a set if len(s) > 1: f[6] = -2 # mixed else: f[6] = s.pop() 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 _on_torrentremoved_event(self, torrent_id): if torrent_id == self.torrentid: self.back_to_overview() def _on_torrentfilerenamed_event(self, torrent_id, index, new_name): if torrent_id == self.torrentid: self.file_dict[index][0] = new_name.split("/")[-1] component.get("SessionProxy").get_torrent_status( self.torrentid, self._status_keys).addCallback(self.set_state) def _on_torrentfolderrenamed_event(self, torrent_id, old_folder, new_folder): if torrent_id == self.torrentid: fe = None fl = None for i in old_folder.strip("/").split("/"): if not fl: fe = fl = self.file_list s = filter(lambda x: x[0].strip("/") == i, fl)[0] fe = s fl = s[3] fe[0] = new_folder.strip("/").rpartition("/")[-1] #self.__get_file_by_name(old_folder, self.file_list)[0] = new_folder.strip("/") component.get("SessionProxy").get_torrent_status( self.torrentid, self._status_keys).addCallback(self.set_state) def draw_files(self, files, depth, off, idx): color_selected = "blue" color_partially_selected = "magenta" color_highlighted = "white" for fl in files: #from sys import stderr #print >> stderr, fl[6] # 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 # default color values fg = "white" bg = "black" attr = "" if fl[6] == -2: priority = -1 #Mixed elif fl[6] == 0: priority = 0 #Do Not Download fg = "red" elif fl[6] == 1: priority = 1 #Normal elif fl[6] <= 6: priority = 2 #High fg = "yellow" elif fl[6] == 7: priority = 3 #Highest fg = "green" if idx >= self.file_off: # set fg/bg colors based on whether the file is selected/marked or not if fl[1] in self.marked: bg = color_selected if fl[3]: if self.marked[ fl[1]] < self.__get_contained_files_count( file_list=fl[3]): bg = color_partially_selected attr = "bold" if idx == self.current_file_idx: self.current_file = fl bg = color_highlighted if fl[1] in self.marked: fg = color_selected if fl[3]: if self.marked[ fl[1]] < self.__get_contained_files_count( file_list=fl[3]): fg = color_partially_selected else: if fg == "white": fg = "black" attr = "bold" if attr: color_string = "{!%s,%s,%s!}" % (fg, bg, attr) else: 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 __get_file_list_length(self, file_list=None): """ Counts length of the displayed file list. """ if file_list == None: file_list = self.file_list length = 0 if file_list: for element in file_list: length += 1 if element[3] and element[4]: length += self.__get_file_list_length(element[3]) return length def __get_contained_files_count(self, file_list=None, idx=None): length = 0 if file_list == None: file_list = self.file_list if idx != None: for element in file_list: if element[1] == idx: return self.__get_contained_files_count( file_list=element[3]) elif element[3]: c = self.__get_contained_files_count(file_list=element[3], idx=idx) if c > 0: return c else: for element in file_list: length += 1 if element[3]: length -= 1 length += self.__get_contained_files_count(element[3]) return length def on_resize(self, *args): BaseMode.on_resize_norefresh(self, *args) #Always refresh Legacy(it will also refresh AllTorrents), otherwise it will bug deluge out legacy = component.get("LegacyUI") legacy.on_resize(*args) self.__update_columns() if self.popup: self.popup.handle_resize() self._listing_start = self.rows / 2 self.refresh() def render_header(self, off): status = self.torrent_state up_color = colors.state_color["Seeding"] down_color = colors.state_color["Downloading"] #Name s = "{!info!}Name: {!input!}%s" % status["name"] self.add_string(off, s) off += 1 #Print DL info and ETA if status["download_payload_rate"] > 0: s = "%sDownloading: {!input!}" % down_color else: s = "{!info!}Downloaded: {!input!}" s += common.fsize(status["all_time_download"]) if status["progress"] != 100.0: s += "/%s" % common.fsize(status["total_wanted"]) if status["download_payload_rate"] > 0: s += " {!yellow!}@ %s%s" % ( down_color, common.fsize(status["download_payload_rate"])) s += "{!info!} ETA: {!input!}%s" % format_utils.format_time( status["eta"]) self.add_string(off, s) off += 1 #Print UL info and ratio if status["upload_payload_rate"] > 0: s = "%sUploading: {!input!}" % up_color else: s = "{!info!}Uploaded: {!input!}" s += common.fsize(status["total_uploaded"]) if status["upload_payload_rate"] > 0: s += " {!yellow!}@ %s%s" % ( up_color, common.fsize(status["upload_payload_rate"])) ratio_str = format_utils.format_float(status["ratio"]) if ratio_str == "-": ratio_str = "inf" s += " {!info!}Ratio: {!input!}%s" % ratio_str self.add_string(off, s) off += 1 #Seeder/leecher info s = "{!info!}Seeders:{!green!} %s {!input!}(%s)" % ( status["num_seeds"], status["total_seeds"]) self.add_string(off, s) off += 1 s = "{!info!}Leechers:{!red!} %s {!input!}(%s)" % ( status["num_peers"], status["total_peers"]) self.add_string(off, s) off += 1 #Tracker if status["message"] == "OK": color = "{!green!}" else: color = "{!red!}" s = "{!info!}Tracker: {!magenta!}%s{!input!} says \"%s%s{!input!}\"" % ( status["tracker_host"], color, status["message"]) self.add_string(off, s) off += 1 #Pieces and availability s = "{!info!}Pieces: {!yellow!}%s {!input!}x {!yellow!}%s" % ( status["num_pieces"], common.fsize(status["piece_length"])) if status["distributed_copies"]: s += " {!info!}Availability: {!input!}%s" % format_utils.format_float( status["distributed_copies"]) self.add_string(off, s) off += 1 #Time added s = "{!info!}Added: {!input!}%s" % common.fdate(status["time_added"]) self.add_string(off, s) off += 1 #Time active s = "{!info!}Time active: {!input!}%s" % (common.ftime( status["active_time"])) if status["seeding_time"]: s += ", {!cyan!}%s{!input!} seeding" % (common.ftime( status["seeding_time"])) self.add_string(off, s) off += 1 #Save Path s = "{!info!}Save path: {!input!}%s" % status["save_path"] self.add_string(off, s) off += 1 #Owner if status["owner"]: s = "{!info!}Owner: {!input!}%s" % status["owner"] return off 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.erase() self.add_string(0, self.statusbars.topbar) #This will quite likely fail when switching modes try: rf = format_utils.remove_formatting string = self.statusbars.bottombar hstr = "Press {!magenta,blue,bold!}[h]{!status!} for help" string += " " * (self.cols - len(rf(string)) - len(rf(hstr))) + hstr self.add_string(self.rows - 1, string) except: pass off = 1 if self.torrent_state: off = self.render_header(off) else: self.add_string(1, "Waiting for torrent state") off += 1 if self.files_sep: self.add_string(off, self.files_sep) off += 1 self._listing_start = off self._listing_space = self.rows - self._listing_start 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) if component.get("ConsoleUI").screen != self: return self.stdscr.noutrefresh() if self.popup: self.popup.refresh() curses.doupdate() def expcol_cur_file(self): """ Expand or collapse current file """ self.current_file[4] = not self.current_file[4] self.refresh() def file_list_down(self, rows=1): maxlen = self.__get_file_list_length() - 1 self.current_file_idx += rows if self.current_file_idx > maxlen: self.current_file_idx = maxlen if self.current_file_idx > self.file_off + (self._listing_space - 3): self.file_off = self.current_file_idx - (self._listing_space - 3) self.refresh() def file_list_up(self, rows=1): self.current_file_idx = max(0, self.current_file_idx - rows) self.file_off = min(self.file_off, self.current_file_idx) self.refresh() def back_to_overview(self): component.stop(["TorrentDetail"]) component.deregister(self) self.stdscr.erase() component.get("ConsoleUI").set_mode(self.alltorrentmode) self.alltorrentmode._go_top = False self.alltorrentmode.resume() # build list of priorities for all files in the torrent # based on what is currently selected and a selected priority. def build_prio_list(self, files, ret_list, parent_prio, selected_prio): # has a priority been set on my parent (if so, I inherit it) for f in files: #Do not set priorities for the whole dir, just selected contents if f[3]: 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, was_empty): plist = [] self.build_prio_list(self.file_list, plist, -1, data) plist.sort() priorities = [p[1] for p in plist] log.debug("priorities: %s", priorities) client.core.set_torrent_file_priorities(self.torrentid, priorities) if was_empty: self.marked = {} return True # show popup for priority selections def show_priority_popup(self, was_empty): func = lambda idx, data, we=was_empty: self.do_priority(idx, data, we) if self.marked: self.popup = SelectablePopup(self, "Set File Priority", func) self.popup.add_line( "_Do Not Download", data=deluge.common.FILE_PRIORITY["Do Not Download"], foreground="red") 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"], foreground="yellow") self.popup.add_line( "H_ighest Priority", data=deluge.common.FILE_PRIORITY["Highest Priority"], foreground="green") self.popup._selected = 1 def __mark_unmark(self, idx): """ Selects or unselects file or a catalog(along with contained files) """ fc = self.__get_contained_files_count(idx=idx) if idx not in self.marked: #Not selected, select it self.__mark_tree(self.file_list, idx) elif self.marked[idx] < fc: #Partially selected, unselect all contents self.__unmark_tree(self.file_list, idx) else: #Selected, unselect it self.__unmark_tree(self.file_list, idx) def __mark_tree(self, file_list, idx, mark_all=False): """ Given file_list of TorrentDetail and index of file or folder, recursively selects all files contained as well as marks folders higher in hierarchy as partially selected """ total_marked = 0 for element in file_list: marked = 0 #Select the file if it's the one we want or # if it's inside a directory that got selected if (element[1] == idx) or mark_all: #If it's a folder then select everything inside if element[3]: marked = self.__mark_tree(element[3], idx, True) self.marked[element[1]] = marked else: marked = 1 self.marked[element[1]] = 1 else: #Does not match but the item to be selected might be inside, recurse if element[3]: marked = self.__mark_tree(element[3], idx, False) #Partially select the folder if it contains files that were selected if marked > 0: self.marked[element[1]] = marked else: if element[1] in self.marked: #It's not the element we want but it's marked so count it marked = 1 #Count and then return total amount of files selected in all subdirectories total_marked += marked return total_marked def __get_file_by_num(self, num, file_list, idx=0): for element in file_list: if idx == num: return element if element[3] and element[4]: i = self.__get_file_by_num(num, element[3], idx + 1) if not isinstance(i, int): return i else: idx = i else: idx += 1 return idx def __get_file_by_name(self, name, file_list, idx=0): for element in file_list: if element[0].strip("/") == name.strip("/"): return element if element[3] and element[4]: i = self.__get_file_by_name(name, element[3], idx + 1) if not isinstance(i, int): return i else: idx = i else: idx += 1 return idx def __unmark_tree(self, file_list, idx, unmark_all=False): """ Given file_list of TorrentDetail and index of file or folder, recursively deselects all files contained as well as marks folders higher in hierarchy as unselected or partially selected """ total_marked = 0 for element in file_list: marked = 0 #It's either the item we want to select or # a contained item, deselect it if (element[1] == idx) or unmark_all: if element[1] in self.marked: del self.marked[element[1]] #Deselect all contents if it's a catalog if element[3]: self.__unmark_tree(element[3], idx, True) else: #Not file we wanted but it might be inside this folder, recurse inside if element[3]: marked = self.__unmark_tree(element[3], idx, False) #If none of the contents remain selected, unselect this folder as well if marked == 0: if element[1] in self.marked: del self.marked[element[1]] #Otherwise update selection count else: self.marked[element[1]] = marked else: if element[1] in self.marked: marked = 1 #Count and then return selection count so we can update # directories higher up in the hierarchy total_marked += marked return total_marked def _selection_to_file_idx(self, file_list=None, idx=0, true_idx=0, closed=False): if not file_list: file_list = self.file_list for element in file_list: if idx == self.current_file_idx: return true_idx #It's a folder if element[3]: i = self._selection_to_file_idx(element[3], idx + 1, true_idx, closed or not element[4]) if isinstance(i, tuple): idx, true_idx = i if element[4]: idx, true_idx = i else: idx += 1 _, true_idx = i else: return i else: if not closed: idx += 1 true_idx += 1 return (idx, true_idx) def _get_full_folder_path(self, num, file_list=None, path="", idx=0): if not file_list: file_list = self.file_list for element in file_list: if not element[3]: idx += 1 continue if num == idx: return "%s%s/" % (path, element[0]) if element[4]: i = self._get_full_folder_path(num, element[3], path + element[0] + "/", idx + 1) if not isinstance(i, int): return i else: idx = i else: idx += 1 return idx def _do_rename_folder(self, torrent_id, folder, new_folder): client.core.rename_folder(torrent_id, folder, new_folder) def _do_rename_file(self, torrent_id, file_idx, new_filename): if not new_filename: return client.core.rename_files(torrent_id, [(file_idx, new_filename)]) def _show_rename_popup(self): #Perhaps in the future: Renaming multiple files if self.marked: title = "Error (Enter to close)" text = "Sorry, you can't rename multiple files, please clear selection with {!info!}'c'{!normal!} key" self.popup = MessagePopup(self, title, text) else: _file = self.__get_file_by_num(self.current_file_idx, self.file_list) old_filename = _file[0] idx = self._selection_to_file_idx() tid = self.torrentid if _file[3]: def do_rename(result): if not result["new_foldername"]: return old_fname = self._get_full_folder_path( self.current_file_idx) new_fname = "%s/%s/" % (old_fname.strip("/").rpartition( "/")[0], result["new_foldername"]) self._do_rename_folder(tid, old_fname, new_fname) popup = InputPopup(self, "Rename folder (Esc to cancel)", close_cb=do_rename) popup.add_text("{!info!}Renaming folder:{!input!}") popup.add_text(" * %s\n" % old_filename) popup.add_text_input("Enter new folder name:", "new_foldername", old_filename.strip("/")) self.popup = popup else: def do_rename(result): fname = "%s/%s" % (self.full_names[idx].rpartition("/")[0], result["new_filename"]) self._do_rename_file(tid, idx, fname) popup = InputPopup(self, "Rename file (Esc to cancel)", close_cb=do_rename) popup.add_text("{!info!}Renaming file:{!input!}") popup.add_text(" * %s\n" % old_filename) popup.add_text_input("Enter new filename:", "new_filename", old_filename) self.popup = popup 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 if not self.torrent_state: # actions below only make sense if there is a torrent state return # Navigate the torrent list if c == curses.KEY_UP: self.file_list_up() elif c == curses.KEY_PPAGE: self.file_list_up(self._listing_space - 2) elif c == curses.KEY_HOME: self.file_off = 0 self.current_file_idx = 0 elif c == curses.KEY_DOWN: self.file_list_down() elif c == curses.KEY_NPAGE: self.file_list_down(self._listing_space - 2) elif c == curses.KEY_END: self.current_file_idx = self.__get_file_list_length() - 1 self.file_off = self.current_file_idx - (self._listing_space - 3) elif c == curses.KEY_DC: torrent_actions_popup(self, [self.torrentid], action=ACTION.REMOVE) # Enter Key elif c == curses.KEY_ENTER or c == 10: was_empty = (self.marked == {}) self.__mark_tree(self.file_list, self.current_file[1]) self.show_priority_popup(was_empty) # 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]) elif chr(c) == 'r': self._show_rename_popup() elif chr(c) == 'c': self.marked = {} elif chr(c) == 'a': torrent_actions_popup(self, [self.torrentid], details=False) return elif chr(c) == 'o': torrent_actions_popup(self, [self.torrentid], action=ACTION.TORRENT_OPTIONS) return elif chr(c) == 'h': self.popup = MessagePopup(self, "Help", HELP_STR, width_req=0.75) elif chr(c) == 'j': self.file_list_up() if chr(c) == 'k': self.file_list_down() self.refresh()
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.erase() 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 (up & down arrows to navigate, esc to cancel)", close_cb=self.__do_add) self.popup.add_text_input("Hostname:", "hostname") self.popup.add_text_input("Port:", "port") self.popup.add_text_input("Username:"******"username") self.popup.add_text_input("Password:"******"password") 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.erase() 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 on_resize(self, *args): BaseMode.on_resize_norefresh(self, *args) if self.popup: self.popup.handle_resize() self.stdscr.erase() self.refresh() 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
class AddTorrents(BaseMode, component.Component): def __init__(self, alltorrentmode, stdscr, console_config, encoding=None): self.console_config = console_config self.alltorrentmode = alltorrentmode self.popup = None self.view_offset = 0 self.cursel = 0 self.marked = set() self.last_mark = -1 path = os.path.expanduser(self.console_config["addtorrents_last_path"]) self.path_stack = ["/"] + path.strip("/").split("/") self.path_stack_pos = len(self.path_stack) self.listing_files = [] self.listing_dirs = [] self.raw_rows = [] self.raw_rows_files = [] self.raw_rows_dirs = [] self.formatted_rows = [] self.sort_column = self.console_config["addtorrents_sort_column"] self.reverse_sort = self.console_config["addtorrents_reverse_sort"] BaseMode.__init__(self, stdscr, encoding) self._listing_space = self.rows - 5 self.__refresh_listing() component.Component.__init__(self, "AddTorrents", 1, depend=["SessionProxy"]) component.start(["AddTorrents"]) curses.curs_set(0) self.stdscr.notimeout(0) # component start/update def start(self): pass def update(self): pass def __refresh_listing(self): path = os.path.join(*self.path_stack[:self.path_stack_pos]) listing = os.listdir(path) self.listing_files = [] self.listing_dirs = [] self.raw_rows = [] self.raw_rows_files = [] self.raw_rows_dirs = [] self.formatted_rows = [] for f in listing: if os.path.isdir(os.path.join(path, f)): if self.console_config["addtorrents_show_hidden_folders"]: self.listing_dirs.append(f) elif f[0] != ".": self.listing_dirs.append(f) elif os.path.isfile(os.path.join(path, f)): if self.console_config["addtorrents_show_misc_files"]: self.listing_files.append(f) elif f.endswith(".torrent"): self.listing_files.append(f) for dirname in self.listing_dirs: row = [] full_path = os.path.join(path, dirname) try: size = len(os.listdir(full_path)) except: size = -1 time = os.stat(full_path).st_mtime row = [dirname, size, time, full_path, 1] self.raw_rows.append(row) self.raw_rows_dirs.append(row) #Highlight the directory we came from if self.path_stack_pos < len(self.path_stack): selected = self.path_stack[self.path_stack_pos] ld = sorted(self.listing_dirs, key=lambda n: n.lower()) c = ld.index(selected) self.cursel = c if (self.view_offset + self._listing_space) <= self.cursel: self.view_offset = self.cursel - self._listing_space for filename in self.listing_files: row = [] full_path = os.path.join(path, filename) size = os.stat(full_path).st_size time = os.stat(full_path).st_mtime row = [filename, size, time, full_path, 0] self.raw_rows.append(row) self.raw_rows_files.append(row) self.__sort_rows() def __sort_rows(self): self.console_config["addtorrents_sort_column"] = self.sort_column self.console_config["addtorrents_reverse_sort"] = self.reverse_sort self.console_config.save() self.raw_rows_dirs.sort(key=lambda r: r[0].lower()) if self.sort_column == "name": self.raw_rows_files.sort(key=lambda r: r[0].lower(), reverse=self.reverse_sort) elif self.sort_column == "date": self.raw_rows_files.sort(key=lambda r: r[2], reverse=self.reverse_sort) self.raw_rows = self.raw_rows_dirs + self.raw_rows_files self.__refresh_rows() def __refresh_rows(self): self.formatted_rows = [] for row in self.raw_rows: filename = row[0] size = row[1] time = row[2] if row[4]: if size != -1: size_str = "%i items" % size else: size_str = " unknown" try: filename = filename.decode("utf8") except: pass cols = [filename, size_str, common.fdate(time)] widths = [self.cols - 35, 12, 23] self.formatted_rows.append( format_utils.format_row(cols, widths)) else: #Size of .torrent file itself couldn't matter less so we'll leave it out try: filename = filename.decode("utf8") except: pass cols = [filename, common.fdate(time)] widths = [self.cols - 23, 23] self.formatted_rows.append( format_utils.format_row(cols, widths)) def scroll_list_up(self, distance): self.cursel -= distance if self.cursel < 0: self.cursel = 0 if self.cursel < self.view_offset + 1: self.view_offset = max(self.cursel - 1, 0) def scroll_list_down(self, distance): self.cursel += distance if self.cursel >= len(self.formatted_rows): self.cursel = len(self.formatted_rows) - 1 if (self.view_offset + self._listing_space) <= self.cursel + 1: self.view_offset = self.cursel - self._listing_space + 1 def set_popup(self, pu): self.popup = pu self.refresh() def on_resize(self, *args): BaseMode.on_resize_norefresh(self, *args) #Always refresh Legacy(it will also refresh AllTorrents), otherwise it will bug deluge out legacy = component.get("LegacyUI") legacy.on_resize(*args) if self.popup: self.popup.handle_resize() self._listing_space = self.rows - 5 self.refresh() def refresh(self, lines=None): # Update the status bars self.stdscr.erase() self.add_string(0, self.statusbars.topbar) #This will quite likely fail when switching modes try: rf = format_utils.remove_formatting string = self.statusbars.bottombar hstr = "Press {!magenta,blue,bold!}[h]{!status!} for help" string += " " * (self.cols - len(rf(string)) - len(rf(hstr))) + hstr self.add_string(self.rows - 1, string) except: pass off = 1 #Render breadcrumbs s = "Location: " for i, e in enumerate(self.path_stack): if e == "/": if i == self.path_stack_pos - 1: s += "{!black,red,bold!}root" else: s += "{!red,black,bold!}root" else: if i == self.path_stack_pos - 1: s += "{!black,white,bold!}%s" % e else: s += "{!white,black,bold!}%s" % e if e != len(self.path_stack) - 1: s += "{!white,black!}/" self.add_string(off, s) off += 1 #Render header cols = ["Name", "Contents", "Modification time"] widths = [self.cols - 35, 12, 23] s = "" for i, (c, w) in enumerate(zip(cols, widths)): cn = "" if i == 0: cn = "name" elif i == 2: cn = "date" if cn == self.sort_column: s += "{!black,green,bold!}" + c.ljust(w - 2) if self.reverse_sort: s += "^ " else: s += "v " else: s += "{!green,black,bold!}" + c.ljust(w) self.add_string(off, s) off += 1 #Render files and folders for i, row in enumerate(self.formatted_rows[self.view_offset:]): i += self.view_offset #It's a folder color_string = "" if self.raw_rows[i][4]: if self.raw_rows[i][1] == -1: if i == self.cursel: color_string = "{!black,red,bold!}" else: color_string = "{!red,black!}" else: if i == self.cursel: color_string = "{!black,cyan,bold!}" else: color_string = "{!cyan,black!}" elif i == self.cursel: if self.raw_rows[i][0] in self.marked: color_string = "{!blue,white,bold!}" else: color_string = "{!black,white,bold!}" elif self.raw_rows[i][0] in self.marked: color_string = "{!white,blue,bold!}" self.add_string(off, color_string + row) off += 1 if off > self.rows - 2: break if component.get("ConsoleUI").screen != self: return self.stdscr.noutrefresh() if self.popup: self.popup.refresh() curses.doupdate() def back_to_overview(self): component.stop(["AddTorrents"]) component.deregister(self) self.stdscr.erase() component.get("ConsoleUI").set_mode(self.alltorrentmode) self.alltorrentmode._go_top = False self.alltorrentmode.resume() def _perform_action(self): if self.cursel < len(self.listing_dirs): self._enter_dir() else: s = self.raw_rows[self.cursel][0] if s not in self.marked: self.last_mark = self.cursel self.marked.add(s) self._show_add_dialog() def _enter_dir(self): #Enter currently selected directory dirname = self.raw_rows[self.cursel][0] new_dir = self.path_stack_pos >= len(self.path_stack) new_dir = new_dir or (dirname != self.path_stack[self.path_stack_pos]) if new_dir: self.path_stack = self.path_stack[:self.path_stack_pos] self.path_stack.append(dirname) path = os.path.join(*self.path_stack[:self.path_stack_pos + 1]) if not os.access(path, os.R_OK): self.path_stack = self.path_stack[:self.path_stack_pos] self.popup = MessagePopup(self, "Error", "{!error!}Access denied: %s" % path) self.__refresh_listing() return self.path_stack_pos += 1 self.view_offset = 0 self.cursel = 0 self.last_mark = -1 self.marked = set() self.__refresh_listing() def _show_add_dialog(self): def _do_add(result): ress = { "succ": 0, "fail": 0, "total": len(self.marked), "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("{!input!} * %s: {!error!}%s" % (t_file, msg)) if (ress["succ"] + ress["fail"]) >= ress["total"]: self.alltorrentmode._report_add_status( ress["succ"], ress["fail"], ress["fmsg"]) def success_cb(tid, t_file, ress): if tid: log.debug("added torrent: %s (%s)" % (t_file, tid)) ress["succ"] += 1 if (ress["succ"] + ress["fail"]) >= ress["total"]: self.alltorrentmode._report_add_status( ress["succ"], ress["fail"], ress["fmsg"]) else: fail_cb("Already in session (probably)", t_file, ress) for m in self.marked: filename = m directory = os.path.join( *self.path_stack[:self.path_stack_pos]) path = os.path.join(directory, filename) filedump = base64.encodestring(open(path).read()) t_options = {} if result["location"]: t_options["download_location"] = result["location"] t_options["add_paused"] = result["add_paused"] d = client.core.add_torrent_file(filename, filedump, t_options) d.addCallback(success_cb, filename, ress) d.addErrback(fail_cb, filename, ress) self.console_config["addtorrents_last_path"] = os.path.join( *self.path_stack[:self.path_stack_pos]) self.console_config.save() self.back_to_overview() config = component.get("ConsoleUI").coreconfig dl = config["download_location"] if config["add_paused"]: ap = 0 else: ap = 1 self.popup = InputPopup(self, "Add Torrents (Esc to cancel)", close_cb=_do_add, height_req=17) msg = "Adding torrent files:" for i, m in enumerate(self.marked): name = m msg += "\n * {!input!}%s" % name if i == 5: if i < len(self.marked): msg += "\n {!red!}And %i more" % (len(self.marked) - 5) break self.popup.add_text(msg) self.popup.add_spaces(1) self.popup.add_text_input("Save Location:", "location", dl) self.popup.add_select_input("Add Paused:", "add_paused", ["Yes", "No"], [True, False], ap) def _go_up(self): #Go up in directory hierarchy if self.path_stack_pos > 1: self.path_stack_pos -= 1 self.view_offset = 0 self.cursel = 0 self.last_mark = -1 self.marked = set() self.__refresh_listing() 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 # Navigate the torrent list if c == curses.KEY_UP: self.scroll_list_up(1) elif c == curses.KEY_PPAGE: #self.scroll_list_up(self._listing_space-2) self.scroll_list_up(self.rows // 2) elif c == curses.KEY_HOME: self.scroll_list_up(len(self.formatted_rows)) elif c == curses.KEY_DOWN: self.scroll_list_down(1) elif c == curses.KEY_NPAGE: #self.scroll_list_down(self._listing_space-2) self.scroll_list_down(self.rows // 2) elif c == curses.KEY_END: self.scroll_list_down(len(self.formatted_rows)) elif c == curses.KEY_RIGHT: if self.cursel < len(self.listing_dirs): self._enter_dir() elif c == curses.KEY_LEFT: self._go_up() # Enter Key elif c == curses.KEY_ENTER or c == 10: self._perform_action() #Escape elif c == 27: self.back_to_overview() else: if c > 31 and c < 256: if chr(c) == 'h': self.popup = MessagePopup(self, "Help", HELP_STR, width_req=0.75) elif chr(c) == '>': if self.sort_column == "date": self.reverse_sort = not self.reverse_sort else: self.sort_column = "date" self.reverse_sort = True self.__sort_rows() elif chr(c) == '<': if self.sort_column == "name": self.reverse_sort = not self.reverse_sort else: self.sort_column = "name" self.reverse_sort = False self.__sort_rows() elif chr(c) == 'm': s = self.raw_rows[self.cursel][0] if s in self.marked: self.marked.remove(s) else: self.marked.add(s) self.last_mark = self.cursel elif chr(c) == 'j': self.scroll_list_up(1) elif chr(c) == 'k': self.scroll_list_down(1) elif chr(c) == 'M': if self.last_mark != -1: if self.last_mark > self.cursel: m = range(self.cursel, self.last_mark) else: m = range(self.last_mark, self.cursel + 1) for i in m: s = self.raw_rows[i][0] self.marked.add(s) elif chr(c) == 'c': self.marked.clear() self.refresh()
class AllTorrents(BaseMode, component.Component): def __init__(self, stdscr, encoding=None): self.torrent_names = None self.numtorrents = -1 self._cached_rows = {} 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.search_state = SEARCH_EMPTY self.coreconfig = component.get("ConsoleUI").coreconfig self.legacy_mode = None self.__status_dict = {} self.__torrent_info_id = None BaseMode.__init__(self, stdscr, encoding) component.Component.__init__(self, "AllTorrents", 1, depend=["SessionProxy"]) curses.curs_set(0) self.stdscr.notimeout(0) self.update_config() component.start(["AllTorrents"]) 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", format_utils.format_float, ("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", format_utils.format_float, ("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"] self.legacy_mode = Legacy(self.stdscr, self.encoding) if self.config["first_run"]: self.popup = Popup(self,"Welcome to Deluge" ,init_lines=self.__help_lines, height_req=0.75, width_req=65) self.config["first_run"] = False self.config.save() # component start/update def start(self): component.get("SessionProxy").get_torrents_status(self.__status_dict, self.__status_fields).addCallback(self.set_state,False) def update(self): component.get("SessionProxy").get_torrents_status(self.__status_dict, self.__status_fields).addCallback(self.set_state,True) if self.__torrent_info_id: component.get("SessionProxy").get_torrent_status(self.__torrent_info_id, self.__status_keys).addCallback(self._on_torrent_status) def update_config(self): self.config = ConfigManager("console.conf",DEFAULT_PREFS) s_primary = self.config["sort_primary"] s_secondary = self.config["sort_secondary"] self.__cols_to_show = [ pref for pref in column_pref_names if ("show_%s" % pref) not in self.config or self.config["show_%s"%pref] ] self.__columns = [prefs_to_names[col] for col in self.__cols_to_show] self.__status_fields = column.get_required_fields(self.__columns) # we always need these, even if we're not displaying them for rf in ["state", "name", "queue", "progress"]: if rf not in self.__status_fields: self.__status_fields.append(rf) # same with sort keys if s_primary and (s_primary not in self.__status_fields): self.__status_fields.append(s_primary) if s_secondary and (s_secondary not in self.__status_fields): self.__status_fields.append(s_secondary) self.__update_columns() def resume(self): component.start(["AllTorrents"]) self.refresh() def __update_columns(self): self.column_widths = [self.config["%s_width"%c] for c in self.__cols_to_show] 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.__columns)) 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)) if (var_cols > 0): vw = int(rem/var_cols) for i in range(0, len(self.column_widths)): if (self.column_widths[i] < 0): self.column_widths[i] = vw self.column_string = "{!header!}" try: primary_sort_col_name = prefs_to_names[self.config["sort_primary"]] except: primary_sort_col_name = "" for i, column in enumerate(self.__columns): ccol = column width = self.column_widths[i] #Trim the column if it's too long to fit if len(ccol) > width: ccol = ccol[:width - 1] # Padding ccol += " " * (width - len(ccol)) # Highlight the primary sort column if column == primary_sort_col_name: if i != len(self.__columns) - 1: ccol = "{!black,green,bold!}%s{!header!}" % ccol else: ccol = ("{!black,green,bold!}%s" % ccol)[:-1] self.column_string += ccol def set_state(self, state, refresh): self.curstate = state # cache in case we change sort order newnames = [] self._cached_rows = {} self._sorted_ids = self._sort_torrents(self.curstate) for torrent_id in self._sorted_ids: ts = self.curstate[torrent_id] newnames.append(ts["name"]) self.numtorrents = len(state) 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]] nl = len(f[0])+4 if (nl+len(info))>self.popup.width: self.popup.add_line("{!info!}%s: {!input!}%s"%(f[0],info[:(self.popup.width - nl)])) info = info[(self.popup.width - nl):] n = self.popup.width-3 chunks = [info[i:i+n] for i in xrange(0, len(info), n)] for c in chunks: self.popup.add_line(" %s"%c) else: self.popup.add_line("{!info!}%s: {!input!}%s"%(f[0],info)) self.refresh() else: self.__torrent_info_id = None def on_resize(self, *args): BaseMode.on_resize_norefresh(self, *args) if self.popup: self.popup.handle_resize() self.update() self.__update_columns() 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 primary and secondary sort fields" if not state: return {} s_primary = self.config["sort_primary"] s_secondary = self.config["sort_secondary"] result = state #Sort first by secondary sort field and then primary sort field # so it all works out cmp_func = self._queue_sort sg = state.get def sort_by_field(state, result, field): if field in column_names_to_state_keys: field = column_names_to_state_keys[field] reverse = field in reverse_sort_fields #Get first element so we can check if it has given field # and if it's a string first_element = state[state.keys()[0]] if field in first_element: is_string = isinstance( first_element[field], basestring) sort_key = lambda s:sg(s)[field] sort_key2 = lambda s:sg(s)[field].lower() #If it's a string, sort case-insensitively but preserve A>a order if is_string: result = sorted(result, cmp_func, sort_key, reverse) result = sorted(result, cmp_func, sort_key2, reverse) else: result = sorted(result, cmp_func, sort_key, reverse) if field == "eta": result = sorted(result, key=lambda s: state.get(s)["eta"] == 0) return result #Just in case primary and secondary fields are empty and/or # both are too ambiguous, also sort by queue position first if "queue" not in [s_secondary, s_primary]: result = sort_by_field(state, result, "queue") if s_secondary != s_primary: result = sort_by_field(state, result, s_secondary) result = sort_by_field(state, result, s_primary) if self.config["separate_complete"]: result = sorted(result, cmp_func, lambda s: state.get(s)["progress"] == 100.0) return result def _format_queue(self, qnum): if (qnum >= 0): return "%d"%(qnum+1) else: return "" def show_addtorrents_screen(self): def dodeets(arg): if arg and True in arg[0]: self.stdscr.erase() component.get("ConsoleUI").set_mode(AddTorrents(self,self.stdscr, self.config, self.encoding)) else: self.messages.append(("Error","An error occured trying to display add torrents screen")) component.stop(["AllTorrents"]).addCallback(dodeets) def show_torrent_details(self,tid): def dodeets(arg): if arg and True in arg[0]: self.stdscr.erase() component.get("ConsoleUI").set_mode(TorrentDetail(self,tid,self.stdscr, self.config, self.encoding)) else: self.messages.append(("Error","An error occured trying to display torrent details")) component.stop(["AllTorrents"]).addCallback(dodeets) 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): def doprefs(arg): if arg and True in arg[0]: self.stdscr.erase() component.get("ConsoleUI").set_mode(Preferences(self,config,self.config,port,status,self.stdscr,self.encoding)) else: self.messages.append(("Error","An error occured trying to display preferences")) component.stop(["AllTorrents"]).addCallback(doprefs) client.core.get_config().addCallback(_on_get_config) def __show_events(self): def doevents(arg): if arg and True in arg[0]: self.stdscr.erase() component.get("ConsoleUI").set_mode(EventView(self,self.stdscr,self.encoding)) else: self.messages.append(("Error","An error occured trying to display events")) component.stop(["AllTorrents"]).addCallback(doevents) def __legacy_mode(self): def dolegacy(arg): if arg and True in arg[0]: self.stdscr.erase() component.get("ConsoleUI").set_mode(self.legacy_mode) self.legacy_mode.refresh() curses.curs_set(2) else: self.messages.append(("Error","An error occured trying to switch to legacy mode")) component.stop(["AllTorrents"]).addCallback(dolegacy) def _torrent_filter(self, idx, data): if data==FILTER.ALL: self.__status_dict = {} self._curr_filter = None elif data==FILTER.ACTIVE: self.__status_dict = {"state":"Active"} self._curr_filter = "Active" elif data==FILTER.DOWNLOADING: self.__status_dict = {"state":"Downloading"} self._curr_filter = "Downloading" elif data==FILTER.SEEDING: self.__status_dict = {"state":"Seeding"} self._curr_filter = "Seeding" elif data==FILTER.PAUSED: self.__status_dict = {"state":"Paused"} self._curr_filter = "Paused" elif data==FILTER.CHECKING: self.__status_dict = {"state":"Checking"} self._curr_filter = "Checking" elif data==FILTER.ERROR: self.__status_dict = {"state":"Error"} self._curr_filter = "Error" elif data==FILTER.QUEUED: self.__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!}Successfully added %d torrent(s)"%succ_cnt) else: msg = ("{!error!}Failed to add the following %d torrent(s):\n {!input!}"%fail_cnt)+"\n ".join(fail_msgs) if succ_cnt != 0: msg += "\n \n{!success!}Successfully added %d torrent(s)"%succ_cnt self.report_message("Torrent Add Report",msg) def _show_torrent_add_popup(self): def do_add_from_url(result): def fail_cb(msg, url): log.debug("failed to add torrent: %s: %s" % (url, msg)) error_msg = "{!input!} * %s: {!error!}%s" % (url, msg) self._report_add_status(0, 1, [error_msg] ) def success_cb(tid, url): if tid: log.debug("added torrent: %s (%s)"%(url, tid)) self._report_add_status(1, 0, []) else: fail_cb("Already in session (probably)", url) url = result["url"] if not url: return t_options = { "download_location": result["path"], "add_paused": result["add_paused"] } if deluge.common.is_magnet(url): client.core.add_torrent_magnet(url, t_options).addCallback(success_cb, url).addErrback(fail_cb, url) elif deluge.common.is_url(url): client.core.add_torrent_url(url, t_options).addCallback(success_cb, url).addErrback(fail_cb, url) else: self.messages.append(("Error","{!error!}Invalid URL or magnet link: %s" % url)) return log.debug("Adding Torrent(s): %s (dl path: %s) (paused: %d)", url, result["path"], result["add_paused"]) def show_add_url_popup(): try: dl = self.coreconfig["download_location"] except KeyError: dl = "" ap = 1 try: if self.coreconfig["add_paused"]: ap = 0 except KeyError: pass self.popup = InputPopup(self,"Add Torrent (Esc to cancel)", close_cb=do_add_from_url) self.popup.add_text_input("Enter torrent URL or Magnet link:", "url") 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 option_chosen(index, data): self.popup = None if not data: return if data == 1: self.show_addtorrents_screen() elif data == 2: show_add_url_popup() self.popup = SelectablePopup(self,"Add torrent", option_chosen) self.popup.add_line("From _File(s)", data=1) self.popup.add_line("From _URL or Magnet", data=2) self.popup.add_line("_Cancel", data=0) def _do_set_column_visibility(self, data): for key, value in data.items(): self.config[key] = value self.config.save() self.update_config() self.__update_columns() self.refresh([]) def _show_visible_columns_popup(self): title = "Visible columns (Enter to exit)" self.popup = InputPopup(self, title, close_cb=self._do_set_column_visibility, immediate_action=True, height_req= len(column_pref_names) + 1, width_req= max([len(col) for col in column_pref_names + [title]]) + 8 ) for col in column_pref_names: name = prefs_to_names[col] prop = "show_%s" % col if prop not in self.config: continue state = self.config[prop] self.popup.add_checked_input(name, prop, state) 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, width_req=1.0) if not lines: if component.get("ConsoleUI").screen != self: return self.stdscr.erase() # 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: string = { SEARCH_EMPTY: "{!black,white!}Search torrents: %s{!black,white!}", SEARCH_SUCCESS: "{!black,white!}Search torrents: {!black,green!}%s{!black,white!}", SEARCH_FAILING: "{!black,white!}Search torrents: {!black,red!}%s{!black,white!}", SEARCH_START_REACHED: "{!black,white!}Search torrents: {!black,yellow!}%s{!black,white!} (start reached)", SEARCH_END_REACHED: "{!black,white!}Search torrents: {!black,yellow!}%s{!black,white!} (end reached)" }[self.search_state] % self.search_string self.add_string(self.rows - 1, string) else: #This will quite likely fail when switching modes try: rf = format_utils.remove_formatting string = self.statusbars.bottombar hstr = "Press {!magenta,blue,bold!}[h]{!status!} for help" string += " " * ( self.cols - len(rf(string)) - len(rf(hstr))) + hstr self.add_string(self.rows - 1, string) except: pass # add all the torrents if self.numtorrents == 0: msg = "No torrents match filter".center(self.cols) self.add_string(3, "{!info!}%s"%msg) elif self.numtorrents > 0: tidx = self.curoff currow = 2 #Because dots are slow sorted_ids = self._sorted_ids curstate = self.curstate gcv = column.get_column_value fr = format_utils.format_row cols = self.__columns colw = self.column_widths cr = self._cached_rows def draw_row(index): if index not in cr: ts = curstate[sorted_ids[index]] cr[index] = (fr([gcv(name,ts) for name in cols],colw),ts["state"]) return cr[index] if lines: todraw = [] for l in lines: if l < tidx - 1: continue if l >= tidx - 1 + self.rows - 3: break if l >= self.numtorrents: break todraw.append(draw_row(l)) lines.reverse() else: todraw = [] for i in range(tidx-1, tidx-1 + self.rows - 3): if i >= self.numtorrents: break todraw += [draw_row(i)] 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 self.entering_search and len(self.search_string) > 1: lcase_name = self.torrent_names[tidx-1].lower() sstring_lower = self.search_string.lower() if lcase_name.find(sstring_lower) != -1: if tidx == self.cursel: pass elif tidx in self.marked: bg = "magenta" else: bg = "green" if fg == "green": fg = "black" attr = "bold" if attr: colorstr = "{!%s,%s,%s!}"%(fg,bg,attr) else: colorstr = "{!%s,%s!}"%(fg,bg) try: self.add_string(currow,"%s%s"%(colorstr,row[0]),trim=False) except: #Yeah, this should be fixed in some better way pass 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, len(self.search_string)+17) else: curses.curs_set(0) if component.get("ConsoleUI").screen != self: return 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 __search_match_count(self): match_count = 0 search_string = self.search_string.lower() for n in self.torrent_names: n = n.lower() if n.find(search_string) != -1: match_count += 1 return match_count def __do_search(self, direction="first", skip=0): """ Performs a search on visible torrent and sets cursor to the match :param string: direction, the direction of search, can be first, last, next or previous :returns: Nothing """ if direction == "first": search_space = enumerate(self.torrent_names) elif direction == "last": search_space = enumerate(self.torrent_names) search_space = list(search_space) search_space = reversed(search_space) elif direction == "next": search_space = enumerate(self.torrent_names) search_space = list(search_space) search_space = search_space[self.cursel:] elif direction == "previous": search_space = enumerate(self.torrent_names) search_space = list(search_space)[:self.cursel-1] search_space = reversed(search_space) search_string = self.search_string.lower() for i,n in search_space: n = n.lower() if n.find(search_string) != -1: if skip > 0: skip -= 1 continue self.cursel = (i+1) if ((self.curoff + self.rows - 5) < self.cursel): self.curoff = self.cursel - self.rows + 5 elif ((self.curoff +1) > self.cursel): self.curoff = max(1, self.cursel - 1) self.search_state = SEARCH_SUCCESS return if direction in ["first", "last"]: self.search_state = SEARCH_FAILING elif direction == "next": self.search_state = SEARCH_END_REACHED elif direction == "previous": self.search_state = SEARCH_START_REACHED def __update_search(self, c): cname = self.torrent_names[self.cursel-1] if c == curses.KEY_BACKSPACE or c == 127: if self.search_string: self.search_string = self.search_string[:-1] if cname.lower().find(self.search_string.lower()) != -1: self.search_state = SEARCH_SUCCESS else: self.entering_search = False self.search_state = SEARCH_EMPTY self.refresh([]) elif c == curses.KEY_DC: self.search_string = "" self.search_state = SEARCH_SUCCESS self.refresh([]) elif c == curses.KEY_UP: self.__do_search("previous") self.refresh([]) elif c == curses.KEY_DOWN: self.__do_search("next") self.refresh([]) elif c == curses.KEY_LEFT: self.entering_search = False self.search_state = SEARCH_EMPTY self.refresh([]) elif c == ord('/'): self.entering_search = False self.search_state = SEARCH_EMPTY self.refresh([]) elif c == curses.KEY_RIGHT: tid = self.current_torrent_id() self.show_torrent_details(tid) elif c == curses.KEY_HOME: self.__do_search("first") self.refresh([]) elif c == curses.KEY_END: self.__do_search("last") self.refresh([]) elif c in [10, curses.KEY_ENTER]: self.last_mark = -1 tid = self.current_torrent_id() torrent_actions_popup(self, [tid] ,details=True) elif c == 27: self.search_string = "" self.search_state = SEARCH_EMPTY self.refresh([]) elif c > 31 and c < 256: old_search_string = self.search_string stroke = chr(c) uchar = "" while not uchar: try: uchar = stroke.decode(self.encoding) except UnicodeDecodeError: c = self.stdscr.getch() stroke += chr(c) if uchar: self.search_string += uchar still_matching = ( cname.lower().find(self.search_string.lower()) == cname.lower().find(old_search_string.lower()) and cname.lower().find(self.search_string.lower()) != -1 ) if self.search_string and not still_matching: self.__do_search() elif self.search_string: self.search_state = SEARCH_SUCCESS self.refresh([]) if not self.search_string: self.search_state = SEARCH_EMPTY self.refresh([]) 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.numtorrents < 0: return elif self.entering_search: self.__update_search(c) return 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_DC: if self.cursel not in self.marked: self.marked.append(self.cursel) self.last_mark = self.cursel torrent_actions_popup(self,self._selected_torrent_ids(), action=ACTION.REMOVE) 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: if self.cursel not in self.marked: 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.entering_search = True elif chr(c) == 'n' and self.search_string: self.__do_search("next") 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: def cb(): self.__torrent_info_id = None self.popup = Popup(self,"Info",close_cb=cb, height_req=20) self.popup.add_line("Getting torrent info...") self.__torrent_info_id = cid elif chr(c) == 'm': self._mark_unmark(self.cursel) effected_lines = [self.cursel-1] elif chr(c) == 'M': if self.last_mark >= 0: if (self.cursel+1) > self.last_mark: mrange = range(self.last_mark,self.cursel+1) else: mrange = range(self.cursel-1,self.last_mark) self.marked.extend(mrange[1:]) effected_lines = mrange 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) == 'v': self._show_visible_columns_popup() elif chr(c) == 'o': if not self.marked: self.marked = [self.cursel] self.last_mark = self.cursel else: self.last_mark = -1 torrent_actions_popup(self, self._selected_torrent_ids(), action=ACTION.TORRENT_OPTIONS) elif chr(c) == '<': i = len(self.__cols_to_show) try: i = self.__cols_to_show.index(self.config["sort_primary"]) - 1 except: pass i = max(0, i) i = min(len(self.__cols_to_show) - 1, i) self.config["sort_primary"] = self.__cols_to_show[i] self.config.save() self.update_config() self.__update_columns() self.refresh([]) elif chr(c) == '>': i = 0 try: i = self.__cols_to_show.index(self.config["sort_primary"]) + 1 except: pass i = min(len(self.__cols_to_show) - 1, i) i = max(0, i) self.config["sort_primary"] = self.__cols_to_show[i] self.config.save() self.update_config() self.__update_columns() self.refresh([]) elif chr(c) == 'f': self._show_torrent_filter_popup() elif chr(c) == 'h': self.popup = MessagePopup(self, "Help", HELP_STR, width_req=0.75) elif chr(c) == 'p': self.show_preferences() return elif chr(c) == 'e': self.__show_events() return elif chr(c) == 'l': self.__legacy_mode() return self.refresh(effected_lines)