class App(Tk): def __init__(self): Tk.__init__(self, className=cst.APP_NAME) self.protocol("WM_DELETE_WINDOW", self.quit) self.withdraw() logging.info('Starting %s', cst.APP_NAME) self.im_icon = PhotoImage(master=self, file=cst.IM_ICON_48) self.iconphoto(True, self.im_icon) # --- style self.style = Style(self) self.style.theme_use("clam") self.style.configure("TScale", sliderlength=20) self.style.map("TCombobox", fieldbackground=[('readonly', 'white')], selectbackground=[('readonly', 'white')], selectforeground=[('readonly', 'black')]) self.style.configure("title.TLabel", font="TkDefaultFont 9 bold") self.style.configure("white.TLabel", background="white") self.style.map("white.TLabel", background=[("active", "white")]) self.style.configure('heading.TLabel', relief='ridge', borderwidth=1, padding=(10, 4)) self.style.configure('manager.TButton', padding=0) self.style.map('manager.Treeview', background=[], foreground=[]) self.style.layout( 'no_edit.TEntry', [('Entry.padding', { 'children': [('Entry.textarea', { 'sticky': 'nswe' })], 'sticky': 'nswe' })]) self.style.configure('no_edit.TEntry', background='white', padding=[4, 0]) self.style.configure('manager.TEntry', padding=[2, 1]) self.style.layout('manager.Treeview.Row', [('Treeitem.row', { 'sticky': 'nswe' }), ('Treeitem.image', { 'side': 'right', 'sticky': 'e' })]) self.style.layout('manager.Treeview.Item', [('Treeitem.padding', { 'children': [('Checkbutton.indicator', { 'side': 'left', 'sticky': '' }), ('Treeitem.text', { 'side': 'left', 'sticky': '' })], 'sticky': 'nswe' })]) self._im_trough = tkPhotoImage(name='trough-scrollbar-vert', width=15, height=15, master=self) bg = CONFIG.get("Widget", 'background', fallback='gray10') widget_bg = (0, 0, 0) widget_fg = (255, 255, 255) vmax = self.winfo_rgb('white')[0] color = tuple(int(val / vmax * 255) for val in widget_bg) active_bg = cst.active_color(color) active_bg2 = cst.active_color(cst.active_color(color, 'RGB')) slider_vert_insens = Image.new('RGBA', (13, 28), widget_bg) slider_vert = Image.new('RGBA', (13, 28), active_bg) slider_vert_active = Image.new('RGBA', (13, 28), widget_fg) slider_vert_prelight = Image.new('RGBA', (13, 28), active_bg2) self._im_trough.put(" ".join(["{" + " ".join([bg] * 15) + "}"] * 15)) self._im_slider_vert_active = PhotoImage(slider_vert_active, name='slider-vert-active', master=self) self._im_slider_vert = PhotoImage(slider_vert, name='slider-vert', master=self) self._im_slider_vert_prelight = PhotoImage(slider_vert_prelight, name='slider-vert-prelight', master=self) self._im_slider_vert_insens = PhotoImage(slider_vert_insens, name='slider-vert-insens', master=self) self.style.element_create('widget.Vertical.Scrollbar.trough', 'image', 'trough-scrollbar-vert') self.style.element_create( 'widget.Vertical.Scrollbar.thumb', 'image', 'slider-vert', ('pressed', '!disabled', 'slider-vert-active'), ('active', '!disabled', 'slider-vert-prelight'), ('disabled', 'slider-vert-insens'), border=6, sticky='ns') self.style.layout('widget.Vertical.TScrollbar', [ ('widget.Vertical.Scrollbar.trough', { 'children': [('widget.Vertical.Scrollbar.thumb', { 'expand': '1' })], 'sticky': 'ns' }) ]) hide = Image.new('RGBA', (12, 12), active_bg2) hide_active = Image.new('RGBA', (12, 12), widget_fg) hide_pressed = Image.new('RGBA', (12, 12), (150, 0, 0)) toggle_open = Image.new('RGBA', (9, 9), widget_fg) toggle_open_active = Image.new('RGBA', (9, 9), active_bg2) toggle_close = Image.new('RGBA', (9, 9), widget_fg) toggle_close_active = Image.new('RGBA', (9, 9), active_bg2) self._im_hide = PhotoImage(hide, master=self) self._im_hide_active = PhotoImage(hide_active, master=self) self._im_hide_pressed = PhotoImage(hide_pressed, master=self) self._im_open = PhotoImage(toggle_open, master=self) self._im_open_active = PhotoImage(toggle_open_active, master=self) self._im_close = PhotoImage(toggle_close, master=self) self._im_close_active = PhotoImage(toggle_close_active, master=self) self.style.element_create( "toggle", "image", self._im_close, ("!hover", "selected", "!disabled", self._im_open), ("hover", "!selected", "!disabled", self._im_close_active), ("hover", "selected", "!disabled", self._im_open_active), border=2, sticky='') self.style.layout('Toggle', [('Toggle.border', { 'children': [('Toggle.padding', { 'children': [('Toggle.toggle', { 'sticky': 'nswe' })], 'sticky': 'nswe' })], 'sticky': 'nswe' })]) self.style.configure('widget.close.TButton', background=bg, relief='flat', image=self._im_hide, padding=0) self.style.map('widget.close.TButton', background=[], relief=[], image=[('active', '!pressed', self._im_hide_active), ('active', 'pressed', self._im_hide_pressed)]) self.option_add('*Toplevel.background', self.style.lookup('TFrame', 'background')) self.option_add('*{app_name}.background'.format(app_name=cst.APP_NAME), self.style.lookup('TFrame', 'background')) self.widget_style_init() # --- tray icon menu self.icon = TrayIcon(cst.ICON) self.menu_widgets = SubMenu(parent=self.icon.menu) self.menu_categories = SubMenu(parent=self.menu_widgets) self.menu_categories.add_command(label=_('Hide all'), command=self.hide_all_cats) self.menu_categories.add_command(label=_('Show all'), command=self.hide_all_cats) self.menu_categories.add_separator() self.menu_feeds = SubMenu(parent=self.menu_widgets) self.menu_feeds.add_command(label=_('Hide all'), command=self.hide_all_feeds) self.menu_feeds.add_command(label=_('Show all'), command=self.show_all_feeds) self.menu_feeds.add_separator() self.menu_widgets.add_command(label=_('Hide all'), command=self.hide_all) self.menu_widgets.add_command(label=_('Show all'), command=self.show_all) self.menu_widgets.add_separator() self.menu_widgets.add_cascade(label=_('Categories'), menu=self.menu_categories) self.menu_widgets.add_cascade(label=_('Feeds'), menu=self.menu_feeds) self.icon.menu.add_cascade(label=_('Widgets'), menu=self.menu_widgets) self.icon.menu.add_command(label=_('Add feed'), command=self.add) self.icon.menu.add_command(label=_('Update feeds'), command=self.feed_update) self.icon.menu.add_command(label=_('Manage feeds'), command=self.feed_manage) self.icon.menu.add_command(label=_("Suspend"), command=self.start_stop) self.icon.menu.add_separator() self.icon.menu.add_command(label=_('Settings'), command=self.settings) self.icon.menu.add_command(label=_("Check for updates"), command=lambda: UpdateChecker(self, True)) self.icon.menu.add_command(label=_("Help"), command=lambda: Help(self)) self.icon.menu.add_command(label=_("About"), command=lambda: About(self)) self.icon.menu.add_command(label=_('Quit'), command=self.quit) self.icon.loop(self) self._notify_no_internet = True self._internet_id = "" self._update_id = "" self._check_add_id = "" self._check_end_update_id = "" self._check_result_update_id = {} self._check_result_init_id = {} self.queues = {} self.threads = {} # --- category widgets self.cat_widgets = {} self.cat_widgets['All'] = CatWidget(self, 'All') self.cat_widgets['All'].event_generate('<Configure>') self.menu_widgets.add_checkbutton(label=_('Latests'), command=self.toggle_latests_widget) cst.add_trace(self.cat_widgets['All'].variable, 'write', self.latests_widget_trace) self.cat_widgets['All'].variable.set( LATESTS.getboolean('All', 'visible')) cats = LATESTS.sections() cats.remove('All') for category in cats: self.cat_widgets[category] = CatWidget(self, category) self.cat_widgets[category].event_generate('<Configure>') self.menu_categories.add_checkbutton( label=category, command=lambda c=category: self.toggle_category_widget(c)) cst.add_trace(self.cat_widgets[category].variable, 'write', lambda *args, c=category: self.cat_widget_trace(c)) self.cat_widgets[category].variable.set( LATESTS.getboolean(category, 'visible')) # --- feed widgets self.feed_widgets = {} for title in FEEDS.sections(): self._check_result_update_id[title] = '' self._check_result_init_id[title] = '' self.queues[title] = Queue(1) self.threads[title] = None self.menu_feeds.add_checkbutton( label=title, command=lambda t=title: self.toggle_feed_widget(t)) self.feed_widgets[title] = FeedWidget(self, title) cst.add_trace(self.feed_widgets[title].variable, 'write', lambda *args, t=title: self.feed_widget_trace(t)) self.feed_widgets[title].variable.set( FEEDS.getboolean(title, 'visible', fallback=True)) self.feed_init() # --- check for updates if CONFIG.getboolean("General", "check_update"): UpdateChecker(self) self.bind_class('TEntry', '<Control-a>', self.entry_select_all) def widget_style_init(self): """Init widgets style.""" bg = CONFIG.get('Widget', 'background', fallback='gray10') feed_bg = CONFIG.get('Widget', 'feed_background', fallback='gray20') fg = CONFIG.get('Widget', 'foreground') vmax = self.winfo_rgb('white')[0] widget_bg = tuple(int(val / vmax * 255) for val in self.winfo_rgb(bg)) widget_fg = tuple(int(val / vmax * 255) for val in self.winfo_rgb(fg)) active_bg = cst.active_color(widget_bg) active_bg2 = cst.active_color(cst.active_color(widget_bg, 'RGB')) slider_alpha = Image.open(cst.IM_SCROLL_ALPHA) slider_vert_insens = Image.new('RGBA', (13, 28), widget_bg) slider_vert = Image.new('RGBA', (13, 28), active_bg) slider_vert.putalpha(slider_alpha) slider_vert_active = Image.new('RGBA', (13, 28), widget_fg) slider_vert_active.putalpha(slider_alpha) slider_vert_prelight = Image.new('RGBA', (13, 28), active_bg2) slider_vert_prelight.putalpha(slider_alpha) self._im_slider_vert_active.paste(slider_vert_active) self._im_slider_vert.paste(slider_vert) self._im_slider_vert_prelight.paste(slider_vert_prelight) self._im_slider_vert_insens.paste(slider_vert_insens) self._im_trough.put(" ".join(["{" + " ".join([bg] * 15) + "}"] * 15)) hide_alpha = Image.open(cst.IM_HIDE_ALPHA) hide = Image.new('RGBA', (12, 12), active_bg) hide.putalpha(hide_alpha) hide_active = Image.new('RGBA', (12, 12), active_bg2) hide_active.putalpha(hide_alpha) hide_pressed = Image.new('RGBA', (12, 12), widget_fg) hide_pressed.putalpha(hide_alpha) toggle_open_alpha = Image.open(cst.IM_OPENED_ALPHA) toggle_open = Image.new('RGBA', (9, 9), widget_fg) toggle_open.putalpha(toggle_open_alpha) toggle_open_active = Image.new('RGBA', (9, 9), active_bg2) toggle_open_active.putalpha(toggle_open_alpha) toggle_close_alpha = Image.open(cst.IM_CLOSED_ALPHA) toggle_close = Image.new('RGBA', (9, 9), widget_fg) toggle_close.putalpha(toggle_close_alpha) toggle_close_active = Image.new('RGBA', (9, 9), active_bg2) toggle_close_active.putalpha(toggle_close_alpha) self._im_hide.paste(hide) self._im_hide_active.paste(hide_active) self._im_hide_pressed.paste(hide_pressed) self._im_open.paste(toggle_open) self._im_open_active.paste(toggle_open_active) self._im_close.paste(toggle_close) self._im_close_active.paste(toggle_close_active) self.style.configure('widget.TFrame', background=bg) self.style.configure('widget.close.TButton', background=bg) # relief='flat', image=self._im_hide, padding=0) # self.style.map('widget.close.TButton', background=[], relief=[], # image=[('active', '!pressed', self._im_hide_active), # ('active', 'pressed', self._im_hide_pressed)]) self.style.configure('widget.interior.TFrame', background=feed_bg) self.style.configure('widget.TSizegrip', background=bg) self.style.configure('widget.Horizontal.TSeparator', background=bg) self.style.configure('widget.TLabel', background=bg, foreground=fg, font=CONFIG.get('Widget', 'font')) self.style.configure('widget.title.TLabel', background=bg, foreground=fg, font=CONFIG.get('Widget', 'font_title')) self.style.configure('widget.TButton', background=bg, foreground=fg, padding=1, relief='flat') self.style.map('widget.TButton', background=[('disabled', active_bg), ('pressed', fg), ('active', active_bg)], foreground=[('pressed', bg)]) # relief=[('pressed', 'sunken')]) # bordercolor=[('pressed', active_bg)], # darkcolor=[('pressed', bg)], # lightcolor=[('pressed', fg)]) self.update_idletasks() def hide_all(self): """Withdraw all widgets.""" for widget in self.cat_widgets.values(): widget.withdraw() for widget in self.feed_widgets.values(): widget.withdraw() def show_all(self): """Deiconify all widgets.""" for widget in self.cat_widgets.values(): widget.deiconify() for widget in self.feed_widgets.values(): widget.deiconify() def hide_all_feeds(self): """Withdraw all feed widgets.""" for widget in self.feed_widgets.values(): widget.withdraw() def show_all_feeds(self): """Deiconify all feed widgets.""" for widget in self.feed_widgets.values(): widget.deiconify() def hide_all_cats(self): """Withdraw all category widgets.""" for cat, widget in self.cat_widgets.items(): if cat != 'All': widget.withdraw() def show_all_cats(self): """Deiconify all category widgets.""" for cat, widget in self.cat_widgets.items(): if cat != 'All': widget.deiconify() def start_stop(self): """Suspend / restart update checks.""" if self.icon.menu.get_item_label(4) == _("Suspend"): after_ids = [ self._update_id, self._check_add_id, self._internet_id, self._check_end_update_id, self._update_id ] after_ids.extend(self._check_result_update_id.values()) after_ids.extend(self._check_result_init_id.values()) for after_id in after_ids: try: self.after_cancel(after_id) except ValueError: pass self.icon.menu.set_item_label(4, _("Restart")) self.icon.menu.disable_item(1) self.icon.menu.disable_item(2) self.icon.change_icon(cst.ICON_DISABLED, 'feedagregator suspended') else: self.icon.menu.set_item_label(4, _("Suspend")) self.icon.menu.enable_item(1) self.icon.menu.enable_item(2) self.icon.change_icon(cst.ICON, 'feedagregator') for widget in self.feed_widgets.values(): widget.clear() self.feed_init() @staticmethod def entry_select_all(event): event.widget.selection_clear() event.widget.selection_range(0, 'end') def test_connection(self): """ Launch update check if there is an internet connection otherwise check again for an internet connection after 30s. """ if cst.internet_on(): logging.info('Connected to Internet') self._notify_no_internet = True for widget in self.feed_widgets.values(): widget.clear() self.feed_init() else: self._internet_id = self.after(30000, self.test_connection) def quit(self): for after_id in self.tk.call('after', 'info'): try: self.after_cancel(after_id) except ValueError: pass for thread in self.threads.values(): try: thread.terminate() except AttributeError: pass for title, widget in self.feed_widgets.items(): FEEDS.set(title, 'visible', str(widget.variable.get())) for cat, widget in self.cat_widgets.items(): LATESTS.set(cat, 'visible', str(widget.variable.get())) try: self.destroy() except TclError: logging.error("Error on quit") self.after(500, self.quit) def feed_widget_trace(self, title): value = self.feed_widgets[title].variable.get() self.menu_feeds.set_item_value(title, value) FEEDS.set(title, 'visible', str(value)) cst.save_feeds() def cat_widget_trace(self, category): value = self.cat_widgets[category].variable.get() self.menu_categories.set_item_value(category, value) LATESTS.set(category, 'visible', str(value)) cst.save_latests() def latests_widget_trace(self, *args): value = self.cat_widgets['All'].variable.get() self.menu_widgets.set_item_value(_('Latests'), value) LATESTS.set('All', 'visible', str(value)) cst.save_latests() def toggle_category_widget(self, category): value = self.menu_categories.get_item_value(category) if value: self.cat_widgets[category].deiconify() else: self.cat_widgets[category].withdraw() self.update_idletasks() def toggle_latests_widget(self): value = self.menu_widgets.get_item_value(_('Latests')) if value: self.cat_widgets['All'].deiconify() else: self.cat_widgets['All'].withdraw() self.update_idletasks() def toggle_feed_widget(self, title): value = self.menu_feeds.get_item_value(title) if value: self.feed_widgets[title].deiconify() else: self.feed_widgets[title].withdraw() self.update_idletasks() def report_callback_exception(self, *args): """Log exceptions.""" err = "".join(traceback.format_exception(*args)) logging.error(err) showerror(_("Error"), str(args[1]), err, True) def settings(self): update_delay = CONFIG.get('General', 'update_delay') splash_supp = CONFIG.get('General', 'splash_supported', fallback=True) dialog = Config(self) self.wait_window(dialog) cst.save_config() self.widget_style_init() splash_change = splash_supp != CONFIG.get('General', 'splash_supported') for widget in self.cat_widgets.values(): widget.update_style() if splash_change: widget.update_position() for widget in self.feed_widgets.values(): widget.update_style() if splash_change: widget.update_position() if update_delay != CONFIG.get('General', 'update_delay'): self.feed_update() def add(self): dialog = Add(self) self.wait_window(dialog) url = dialog.url self.feed_add(url) def category_remove(self, category): self.cat_widgets[category].destroy() del self.cat_widgets[category] self.menu_categories.delete(category) LATESTS.remove_section(category) cst.save_feeds() cst.save_latests() @staticmethod def feed_get_info(url, queue, mode='latest'): feed = feedparser.parse(url) feed_title = feed['feed'].get('title', '') entries = feed['entries'] today = datetime.now().strftime('%Y-%m-%d %H:%M') if entries: entry_title = entries[0].get('title', '') summary = entries[0].get('summary', '') link = entries[0].get('link', '') latest = """<p id=title>{}</p>\n{}""".format(entry_title, summary) if 'updated' in entries[0]: updated = entries[0].get('updated') else: updated = entries[0].get('published', today) updated = dateutil.parser.parse( updated, tzinfos=cst.TZINFOS).strftime('%Y-%m-%d %H:%M') else: entry_title = "" summary = "" link = "" latest = "" updated = today if mode == 'all': data = [] for entry in entries: title = entry.get('title', '') summary = entry.get('summary', '') if 'updated' in entry: date = entry.get('updated') else: date = entry.get('published', today) date = dateutil.parser.parse( date, tzinfos=cst.TZINFOS).strftime('%Y-%m-%d %H:%M') link = entry.get('link', '') data.append((title, date, summary, link)) queue.put((feed_title, latest, updated, data)) else: queue.put( (feed_title, latest, updated, entry_title, summary, link)) def _check_result_add(self, thread, queue, url, manager_queue=None): if thread.is_alive(): self._check_add_id = self.after(1000, self._check_result_add, thread, queue, url, manager_queue) else: title, latest, date, data = queue.get(False) if title: try: # check if feed's title already exists FEEDS.add_section(title) except configparser.DuplicateSectionError: i = 2 duplicate = True while duplicate: # increment i until title~#i does not already exist try: FEEDS.add_section("{}~#{}".format(title, i)) except configparser.DuplicateSectionError: i += 1 else: duplicate = False name = "{}~#{}".format(title, i) else: name = title if manager_queue is not None: manager_queue.put(name) logging.info("Added feed '%s' %s", name, url) if CONFIG.getboolean("General", "notifications", fallback=True): run([ "notify-send", "-i", cst.IM_ICON_SVG, name, cst.html2text(latest) ]) self.cat_widgets['All'].entry_add(name, date, latest, url) filename = cst.new_data_file() cst.save_data(filename, latest, data) FEEDS.set(name, 'url', url) FEEDS.set(name, 'updated', date) FEEDS.set(name, 'data', filename) FEEDS.set(name, 'visible', 'True') FEEDS.set(name, 'geometry', '') FEEDS.set(name, 'position', 'normal') FEEDS.set(name, 'category', '') FEEDS.set(name, 'sort_is_reversed', 'False') FEEDS.set(name, 'active', 'True') cst.save_feeds() self.queues[name] = queue self.feed_widgets[name] = FeedWidget(self, name) self.menu_feeds.add_checkbutton( label=name, command=lambda: self.toggle_feed_widget(name)) cst.add_trace(self.feed_widgets[name].variable, 'write', lambda *args: self.feed_widget_trace(name)) self.feed_widgets[name].variable.set(True) for entry_title, date, summary, link in data: self.feed_widgets[name].entry_add(entry_title, date, summary, link, -1) else: if manager_queue is not None: manager_queue.put('') if cst.internet_on(): logging.error('%s is not a valid feed.', url) showerror(_('Error'), _('{url} is not a valid feed.').format(url=url)) else: logging.warning('No Internet connection.') showerror(_('Error'), _('No Internet connection.')) def feed_add(self, url, manager=False): """ Add feed with given url. manager: whether this command is run from the feed manager. """ if url: queue = Queue(1) manager_queue = Queue(1) if manager else None thread = Process(target=self.feed_get_info, args=(url, queue, 'all'), daemon=True) thread.start() self._check_result_add(thread, queue, url, manager_queue) if manager: return manager_queue def feed_set_active(self, title, active): FEEDS.set(title, 'active', str(active)) cst.save_feeds() cat = FEEDS.get(title, 'category', fallback='') if active: self.menu_feeds.enable_item(title) if FEEDS.getboolean(title, 'visible'): self.feed_widgets[title].deiconify() if cat != '': self.cat_widgets[cat].show_feed(title) self.cat_widgets['All'].show_feed(title) self._feed_update(title) else: self.menu_feeds.disable_item(title) self.feed_widgets[title].withdraw() if cat != '': self.cat_widgets[cat].hide_feed(title) self.cat_widgets['All'].hide_feed(title) def feed_change_cat(self, title, old_cat, new_cat): if old_cat != new_cat: FEEDS.set(title, 'category', new_cat) if old_cat != '': self.cat_widgets[old_cat].remove_feed(title) if new_cat != '': if new_cat not in LATESTS.sections(): LATESTS.add_section(new_cat) LATESTS.set(new_cat, 'visible', 'True') LATESTS.set(new_cat, 'geometry', '') LATESTS.set(new_cat, 'position', 'normal') LATESTS.set(new_cat, 'sort_order', 'A-Z') self.cat_widgets[new_cat] = CatWidget(self, new_cat) self.cat_widgets[new_cat].event_generate('<Configure>') self.menu_categories.add_checkbutton( label=new_cat, command=lambda: self.toggle_category_widget(new_cat)) cst.add_trace(self.cat_widgets[new_cat].variable, 'write', lambda *args: self.cat_widget_trace(new_cat)) self.cat_widgets[new_cat].variable.set(True) else: try: filename = FEEDS.get(title, 'data') latest = cst.feed_get_latest(filename) except (configparser.NoOptionError, pickle.UnpicklingError): latest = '' self.cat_widgets[new_cat].entry_add( title, FEEDS.get(title, 'updated'), latest, FEEDS.get(title, 'url')) def feed_rename(self, old_name, new_name): options = { opt: FEEDS.get(old_name, opt) for opt in FEEDS.options(old_name) } FEEDS.remove_section(old_name) try: # check if feed's title already exists FEEDS.add_section(new_name) except configparser.DuplicateSectionError: i = 2 duplicate = True while duplicate: # increment i until new_name~#i does not already exist try: FEEDS.add_section("{}~#{}".format(new_name, i)) except configparser.DuplicateSectionError: i += 1 else: duplicate = False name = "{}~#{}".format(new_name, i) else: name = new_name logging.info("Renamed feed '%s' to '%s'", old_name, name) for opt, val in options.items(): FEEDS.set(name, opt, val) self._check_result_init_id[name] = self._check_result_init_id.pop( old_name, '') self._check_result_update_id[name] = self._check_result_update_id.pop( old_name, '') self.threads[name] = self.threads.pop(old_name, None) self.queues[name] = self.queues.pop(old_name) self.feed_widgets[name] = self.feed_widgets.pop(old_name) self.feed_widgets[name].rename_feed(name) self.cat_widgets['All'].rename_feed(old_name, name) category = FEEDS.get(name, 'category', fallback='') if category != '': self.cat_widgets[category].rename_feed(old_name, name) self.menu_feeds.delete(old_name) self.menu_feeds.add_checkbutton( label=name, command=lambda: self.toggle_feed_widget(name)) trace_info = cst.info_trace(self.feed_widgets[name].variable) if trace_info: cst.remove_trace(self.feed_widgets[name].variable, 'write', trace_info[0][1]) cst.add_trace(self.feed_widgets[name].variable, 'write', lambda *args: self.feed_widget_trace(name)) self.menu_feeds.set_item_value(name, self.feed_widgets[name].variable.get()) cst.save_feeds() return name def feed_remove(self, title): self.feed_widgets[title].destroy() del self.queues[title] try: del self.threads[title] except KeyError: pass del self.feed_widgets[title] try: del self._check_result_init_id[title] except KeyError: pass try: del self._check_result_update_id[title] except KeyError: pass try: os.remove(os.path.join(cst.PATH_DATA, FEEDS.get(title, 'data'))) except FileNotFoundError: pass self.menu_feeds.delete(title) logging.info("Removed feed '%s' %s", title, FEEDS.get(title, 'url')) category = FEEDS.get(title, 'category', fallback='') self.cat_widgets['All'].remove_feed(title) if category != '': self.cat_widgets[category].remove_feed(title) FEEDS.remove_section(title) def feed_manage(self): dialog = Manager(self) self.wait_window(dialog) self.update_idletasks() cst.save_latests() if dialog.change_made: cst.save_feeds() self.feed_update() def feed_init(self): """Update feeds.""" for title in FEEDS.sections(): if FEEDS.getboolean(title, 'active', fallback=True): logging.info("Updating feed '%s'", title) self.threads[title] = Process(target=self.feed_get_info, args=(FEEDS.get(title, 'url'), self.queues[title], 'all'), daemon=True) self.threads[title].start() self._check_result_init(title) self._check_end_update_id = self.after(2000, self._check_end_update) def _check_result_init(self, title): if self.threads[title].is_alive(): self._check_result_init_id[title] = self.after( 1000, self._check_result_init, title) else: t, latest, updated, data = self.queues[title].get() if not t: if cst.internet_on(): run([ "notify-send", "-i", "dialog-error", _("Error"), _('{url} is not a valid feed.').format( url=FEEDS.get(title, 'url')) ]) logging.error('%s is not a valid feed.', FEEDS.get(title, 'url')) else: if self._notify_no_internet: run([ "notify-send", "-i", "dialog-error", _("Error"), _('No Internet connection.') ]) logging.warning('No Internet connection') self._notify_no_internet = False self._internet_id = self.after(30000, self.test_connection) after_ids = [ self._update_id, self._check_add_id, self._check_end_update_id, self._update_id ] after_ids.extend(self._check_result_update_id.values()) after_ids.extend(self._check_result_init_id.values()) for after_id in after_ids: try: self.after_cancel(after_id) except ValueError: pass else: date = datetime.strptime(updated, '%Y-%m-%d %H:%M') if (date > datetime.strptime(FEEDS.get(title, 'updated'), '%Y-%m-%d %H:%M') or not FEEDS.has_option(title, 'data')): if CONFIG.getboolean("General", "notifications", fallback=True): run([ "notify-send", "-i", cst.IM_ICON_SVG, title, cst.html2text(latest) ]) FEEDS.set(title, 'updated', updated) category = FEEDS.get(title, 'category', fallback='') self.cat_widgets['All'].update_display( title, latest, updated) if category != '': self.cat_widgets[category].update_display( title, latest, updated) logging.info("Updated feed '%s'", title) self.feed_widgets[title].clear() for entry_title, date, summary, link in data: self.feed_widgets[title].entry_add( entry_title, date, summary, link, -1) logging.info("Populated widget for feed '%s'", title) self.feed_widgets[title].event_generate('<Configure>') self.feed_widgets[title].sort_by_date() try: filename = FEEDS.get(title, 'data') except configparser.NoOptionError: filename = cst.new_data_file() FEEDS.set(title, 'data', filename) cst.save_feeds() cst.save_data(filename, latest, data) else: logging.info("Feed '%s' is up-to-date", title) def _feed_update(self, title): """Update feed with given title.""" logging.info("Updating feed '%s'", title) self.threads[title] = Process(target=self.feed_get_info, args=(FEEDS.get(title, 'url'), self.queues[title]), daemon=True) self.threads[title].start() self._check_result_update(title) def feed_update(self): """Update all feeds.""" try: self.after_cancel(self._update_id) except ValueError: pass for thread in self.threads.values(): try: thread.terminate() except AttributeError: pass self.threads.clear() for title in FEEDS.sections(): if FEEDS.getboolean(title, 'active', fallback=True): self._feed_update(title) self._check_end_update_id = self.after(2000, self._check_end_update) def _check_result_update(self, title): if self.threads[title].is_alive(): self._check_result_update_id[title] = self.after( 1000, self._check_result_update, title) else: t, latest, updated, entry_title, summary, link = self.queues[ title].get(False) if not t: if cst.internet_on(): run([ "notify-send", "-i", "dialog-error", _("Error"), _('{url} is not a valid feed.').format( url=FEEDS.get(title, 'url')) ]) logging.error('%s is not a valid feed.', FEEDS.get(title, 'url')) else: if self._notify_no_internet: run([ "notify-send", "-i", "dialog-error", _("Error"), _('No Internet connection.') ]) logging.warning('No Internet connection') self._notify_no_internet = False self._internet_id = self.after(30000, self.test_connection) after_ids = [ self._update_id, self._check_add_id, self._check_end_update_id, self._update_id ] after_ids.extend(self._check_result_update_id.values()) after_ids.extend(self._check_result_init_id.values()) for after_id in after_ids: try: self.after_cancel(after_id) except ValueError: pass else: date = datetime.strptime(updated, '%Y-%m-%d %H:%M') if date > datetime.strptime(FEEDS.get(title, 'updated'), '%Y-%m-%d %H:%M'): logging.info("Updated feed '%s'", title) if CONFIG.getboolean("General", "notifications", fallback=True): run([ "notify-send", "-i", cst.IM_ICON_SVG, title, cst.html2text(latest) ]) FEEDS.set(title, 'updated', updated) category = FEEDS.get(title, 'category', fallback='') self.cat_widgets['All'].update_display( title, latest, updated) if category != '': self.cat_widgets[category].update_display( title, latest, updated) self.feed_widgets[title].entry_add(entry_title, updated, summary, link, 0) self.feed_widgets[title].sort_by_date() try: filename = FEEDS.get(title, 'data') old, data = cst.load_data(filename) except pickle.UnpicklingError: cst.save_data(filename, latest, [(entry_title, updated, summary, link)]) except configparser.NoOptionError: filename = cst.new_data_file() FEEDS.set(title, 'data', filename) cst.save_data(filename, latest, [(entry_title, updated, summary, link)]) else: data.insert(0, (entry_title, updated, summary, link)) cst.save_data(filename, latest, data) else: logging.info("Feed '%s' is up-to-date", title) def _check_end_update(self): b = [t.is_alive() for t in self.threads.values() if t is not None] if sum(b): self._check_end_update_id = self.after(1000, self._check_end_update) else: cst.save_feeds() for widget in self.cat_widgets.values(): widget.sort() self._update_id = self.after( CONFIG.getint("General", "update_delay"), self.feed_update)