def init(self, pad, callbacks): TextBox.init(self, pad, callbacks) self.quote_rgx = re.compile("[\\\"](.*?)[\\\"]") on_hook("curses_opt_change", self.on_opt_change, self) on_hook("curses_var_change", self.on_var_change, self) args = { "link-list": ("", self.type_link_list), } cmds = { "goto": (self.cmd_goto, ["link-list"], "Goto a specific link"), "destroy": (self.cmd_destroy, [], "Destroy this window"), "show-links": (self.cmd_show_links, [], "Toggle link list display"), "show-summary": (self.cmd_show_desc, [], "Toggle summary display"), "show-enclosures": (self.cmd_show_encs, [], "Toggle enclosure list display") } register_arg_types(self, args) register_commands(self, cmds, "Reader") self.plugin_class = ReaderPlugin self.update_plugin_lookups()
def edit_extras(self, body, extra_content): sel = self.reader.callbacks["get_var"]("reader_item") if not sel: return # If we have the attrs, or are already waiting, bail. if self.got_attrs: return self._dofuncs(body, extra_content, sel) elif self.setup_hook: return (body, extra_content) item_url = eval(sel.id)["URL"] for extra in enabled_extras: url_rgx, needed, func = hacks[extra] if url_rgx.match(item_url): for a in needed: if a not in sel.content and a not in self.needed_attrs: self.needed_attrs.append(a) self.do_funcs.append(func) if self.needed_attrs != []: on_hook("curses_attributes", self.on_attributes) tag_updater.request_attributes(sel.id, self.needed_attrs) self.setup_hook = True return (body, extra_content) else: self.got_attrs = True return self._dofuncs(body, extra_content, sel)
def init(self, pad, callbacks, var): TextBox.init(self, pad, callbacks, False) unregister_command(self, "bind") self.var = var self.value = self.callbacks["get_var"](var) on_hook("curses_var_change", self.on_var_change, self)
def __init__(self, user_queue, callbacks, types = [InputBox, TagList]): CommandHandler.__init__(self) self.plugin_class = ScreenPlugin self.update_plugin_lookups() self.user_queue = user_queue self.callbacks = callbacks self.layout = "default" self.window_types = types self.stdscr = curses.initscr() if self.curses_setup() < 0: return -1 self.pseudo_input_box = curses.newpad(1,1) self.pseudo_input_box.keypad(1) self.pseudo_input_box.nodelay(1) self.input_lock = Lock() self.input_box = None self.sub_edit = False self.floats = [] self.tiles = [] self.windows = [] self.subwindows() # Start grabbing user input self.start_input_thread() on_hook("curses_opt_change", self.screen_opt_change)
def __init__(self, backend): self.plugin_attrs = { "cmd_sync" : self.cmd_sync, "cmd_syncto" : self.cmd_syncto } self.backend = backend # Plugin __init__ happens extremely early so that plugin types can be # used in validating configuration, etc. We use the daemon_serving hook # to do our work after the config and storage is setup. on_hook("daemon_serving", self.setup)
def __init__(self, backend): self.plugin_attrs = { "cmd_sync": self.cmd_sync, "cmd_syncto": self.cmd_syncto } self.backend = backend # Plugin __init__ happens extremely early so that plugin types can be # used in validating configuration, etc. We use the daemon_serving hook # to do our work after the config and storage is setup. on_hook("daemon_serving", self.setup)
def init(self, pad, callbacks): # Drawing information self.pad = pad self.height, self.width = self.pad.getmaxyx() # Callback information self.callbacks = callbacks # Holster for a list of items for batch operations. self.got_items = None self.first_sel = None self.first_story = None self.last_story = None self.tags = [] # Hooks on_hook("curses_eval_tags_changed", self.refresh) on_hook("curses_items_added", self.on_items_added) on_hook("curses_items_removed", self.on_items_removed) on_hook("curses_opt_change", self.on_opt_change) self.update_tag_lists()
def check(self): hooks.on_hook("test", self.hook_a) # No key hooks.on_hook("test", self.hook_b, "first_remove") hooks.on_hook("test", self.hook_c, "first_remove") hooks.on_hook("test2", self.hook_a, "second_remove") hooks.call_hook("test", []) if self.test_set != "abc": raise Exception("Basic hook test failed: %s" % self.test_set) self.test_set = "" hooks.call_hook("test2", []) if self.test_set != "a": raise Exception("Basic hook test2 failed: %s" % self.test_set) self.test_set = "" hooks.unhook_all("first_remove") hooks.call_hook("test", []) if self.test_set != "a": raise Exception("unhook_all failed: %s" % self.test_set) self.test_set = "" hooks.remove_hook("test", self.hook_a) hooks.call_hook("test", []) if self.test_set != "": raise Exception("remove_hook failed: %s" % self.test_set) hooks.call_hook("test2", []) if self.test_set != "a": raise Exception("improper hook removed: %s" % self.test_set) hooks.unhook_all("second_remove") if hooks.hooks != {}: raise Exception("hooks.hooks should be empty! %s" % hooks.hooks) hooks.on_hook("argtest", self.hook_args) for args in [ [], ["abc"], [1, 2, 3] ]: self.test_args = [] hooks.call_hook("argtest", args) if self.test_args != tuple(args): raise Exception("hook arguments failed in %s out %s" % (args, self.test_args)) return True
def edit_debug(self, body, extra_content): sel = self.reader.callbacks["get_var"]("reader_item") if not sel: return if not self.got_attrs: on_hook("curses_attributes", self.on_attributes) tag_updater.request_attributes(sel.id, []) else: extra_content += '\n\n' for k in sel.content.keys(): line = '[%s]: %s' % (k, sel.content[k]) extra_content += prep_for_display(line) + '\n' return (body, extra_content)
def edit_debug(self, body, extra_content): sel = self.reader.callbacks["get_var"]("reader_item") if not sel: return if not self.got_attrs: on_hook("curses_attributes", self.on_attributes) tag_updater.request_attributes(sel.id, []) else: extra_content += '\n\n' for k in sel.content.keys(): line = '[%s]: %s' % (k, sel.content[k]) extra_content += prep_for_display(line) + '\n' return(body, extra_content)
def __init__(self, callbacks, types = [InputBox, TagList]): CommandHandler.__init__(self) self.plugin_class = ScreenPlugin self.update_plugin_lookups() self.callbacks = callbacks self.layout = "default" self.window_types = types self.stdscr = curses.initscr() if self.curses_setup() < 0: return -1 self.pseudo_input_box = curses.newpad(1,1) self.pseudo_input_box.keypad(1) self.pseudo_input_box.nodelay(1) self.input_lock = Lock() set_redisplay_callback(self.readline_redisplay) set_getc(self.readline_getc) # See Python bug 2675, readline + curses os.unsetenv('LINES') os.unsetenv('COLUMNS') self.floats = [] self.tiles = [] self.windows = [] self.subwindows() args = { "color_idx" : ("[color idx] between 1 and 256 ( >8 will be ignored on 8 color terminals)", self.type_color_idx), "fg-color" : ("[fg-color] Foreground color", self.type_color), "bg-color" : ("[bg-color] Background color (optional)\n\nNamed colors: white black red yellow green blue magenta pink\nNumeric colors: 1-256", self.type_color), } cmds = { "color": (self.cmd_color, ["color_idx", "fg-color", "bg-color"], "Change the color palette"), } register_arg_types(self, args) register_commands(self, cmds, "Theme") on_hook("curses_opt_change", self.screen_opt_change)
def init(self, backend): SubThread.init(self, backend) self.item_tag = None self.item_buf = [] self.item_removes = [] self.item_adds = [] self.attributes = {} self.lock = RWLock("tagupdater") # Response counters self.discard = 0 self.still_updating = 0 self.start_pthread() # Setup automatic attributes. # We know we're going to want at least these attributes for # all stories, as they're part of the fallback format string. self.needed_attrs = [ "title", "canto-state", "canto-tags", "link", "enclosures" ] # Make sure we grab attributes needed for the story # format and story format. sfa = config.get_opt("story.format_attrs") tsa = config.get_opt("taglist.search_attributes") for attrlist in [ sfa, tsa ]: for sa in attrlist: if sa not in self.needed_attrs: self.needed_attrs.append(sa) self.write("AUTOATTR", self.needed_attrs) # Lock config_lock so that strtags doesn't change and we miss # tags. config_lock.acquire_read() strtags = config.get_var("strtags") # Request initial information, instantiate TagCores() self.write("WATCHTAGS", strtags) for tag in strtags: self.prot_tagchange(tag) TagCore(tag) on_hook("curses_new_tag", self.on_new_tag) on_hook("curses_del_tag", self.on_del_tag) on_hook("curses_stories_removed", self.on_stories_removed) on_hook("curses_def_opt_change", self.on_def_opt_change) config_lock.release_read()
def grab_it(self): # Prepare temporary files # Get a base filename (sans query strings, etc.) from the URL tmppath = urllib.parse.urlparse(self.href).path fname = os.path.basename(tmppath) # Grab a temporary directory. This allows us to create a file with # an unperturbed filename so scripts can freely use regex / # extension matching in addition to mimetype detection. tmpdir = tempfile.mkdtemp(prefix="canto-") tmpnam = tmpdir + '/' + fname log.debug("Downloading %s to %s" % (self.href, tmpnam)) on_hook("curses_exit", lambda : (os.unlink(tmpnam))) on_hook("curses_exit", lambda : (os.rmdir(tmpdir))) tmp = open(tmpnam, 'w+b') # Set these because some sites think python's urllib is a scraper and # 403 it. extra_headers = { 'User-Agent' :\ 'Canto/0.9.0 + http://codezen.org/canto-ng'} request = urllib.request.Request(self.href, headers = extra_headers) # Grab the HTTP info / prepare to read. response = urllib.request.urlopen(request) # Grab in kilobyte chunks to avoid wasting memory on something # that's going to be immediately written to disk. while True: r = response.read(1024) if not r: break tmp.write(r) response.close() tmp.close() return tmpnam
def grab_it(self): # Prepare temporary files # Get a base filename (sans query strings, etc.) from the URL tmppath = urllib.parse.urlparse(self.href).path fname = os.path.basename(tmppath) # Grab a temporary directory. This allows us to create a file with # an unperturbed filename so scripts can freely use regex / # extension matching in addition to mimetype detection. tmpdir = tempfile.mkdtemp(prefix="canto-") tmpnam = tmpdir + '/' + fname log.debug("Downloading %s to %s", self.href, tmpnam) on_hook("curses_exit", lambda : (os.unlink(tmpnam))) on_hook("curses_exit", lambda : (os.rmdir(tmpdir))) tmp = open(tmpnam, 'w+b') # Set these because some sites think python's urllib is a scraper and # 403 it. extra_headers = { 'User-Agent' :\ 'Canto/0.9.0 + http://codezen.org/canto-ng'} request = urllib.request.Request(self.href, headers = extra_headers) # Grab the HTTP info / prepare to read. response = urllib.request.urlopen(request) # Grab in kilobyte chunks to avoid wasting memory on something # that's going to be immediately written to disk. while True: r = response.read(1024) if not r: break tmp.write(r) response.close() tmp.close() return tmpnam
def __init__(self, id, callbacks): PluginHandler.__init__(self) self.plugin_class = StoryPlugin self.update_plugin_lookups() self.callbacks = callbacks self.content = {} self.id = id self.pad = None self.selected = False self.marked = False # Are there changes pending? self.changed = True # Information from last refresh self.width = 0 self.lines = 0 # Lines not in our pad, but placed after us (tag footer) self.extra_lines = 0 # Offset globally and in-tag. self.offset = 0 self.rel_offset = 0 on_hook("curses_opt_change", self.on_opt_change) on_hook("curses_tag_opt_change", self.on_tag_opt_change) on_hook("curses_attributes", self.on_attributes)
def init(self, pad, callbacks): TextBox.init(self, pad, callbacks) self.quote_rgx = re.compile("[\\\"](.*?)[\\\"]") on_hook("curses_opt_change", self.on_opt_change, self) on_hook("curses_var_change", self.on_var_change, self) args = { "link-list" : ("", self.type_link_list), } cmds = { "goto" : (self.cmd_goto, ["link-list"], "Goto a specific link"), "destroy" : (self.cmd_destroy, [], "Destroy this window"), "show-links" : (self.cmd_show_links, [], "Toggle link list display"), "show-summary" : (self.cmd_show_desc, [], "Toggle summary display"), "show-enclosures" : (self.cmd_show_encs, [], "Toggle enclosure list display") } register_arg_types(self, args) register_commands(self, cmds, "Reader") self.plugin_class = ReaderPlugin self.update_plugin_lookups()
def __init__(self, tag, callbacks): list.__init__(self) self.tag = tag self.pad = None self.footpad = None # Note that Tag() is only given the top-level CantoCursesGui # callbacks as it shouldn't be doing input / refreshing # itself. self.callbacks = callbacks.copy() # Modify our own callbacks so that *_tag_opt assumes # the current tag. self.callbacks["get_tag_opt"] =\ lambda x : callbacks["get_tag_opt"](self, x) self.callbacks["set_tag_opt"] =\ lambda x, y : callbacks["set_tag_opt"](self, x, y) self.callbacks["get_tag_name"] = lambda : self.tag # This could be implemented as a generic, top-level hook but then N # tags would have access to story objects they shouldn't have and # would have to check every items membership in self, which would be # pointless and time-consuming. self.callbacks["item_state_change"] =\ self.on_item_state_change # Are there changes pending? self.changed = True self.selected = False self.marked = False # Information from last refresh self.lines = 0 self.footlines = 0 self.extra_lines = 0 self.width = 0 # Global indices (for enumeration) self.item_offset = 0 self.visible_tag_offset = 0 self.tag_offset = 0 self.sel_offset = 0 on_hook("curses_opt_change", self.on_opt_change) on_hook("curses_tag_opt_change", self.on_tag_opt_change) on_hook("curses_attributes", self.on_attributes) # Upon creation, this Tag adds itself to the # list of all tags. callbacks["get_var"]("alltags").append(self)
def setup(self): # Use setattributes and setconfigs commands to determine that we are the fresh # copy that should be synchronized. self.sync_ts = 0; on_hook("daemon_end_loop", self.loop) on_hook("daemon_pre_setconfigs", self.pre_setconfigs) on_hook("daemon_pre_setattributes", self.pre_setattributes) on_hook("daemon_exit", self.cmd_syncto) self.reset() # Do the initial sync # sync will grab files, check the timediff on the file if the file is # actually newer (like we failed to sync last time) then it will set # fresh_config and do a syncto. self.cmd_sync()
def setup(self): # Use setattributes and setconfigs commands to determine that we are the fresh # copy that should be synchronized. on_hook("daemon_end_loop", self.loop) on_hook("daemon_pre_setconfigs", self.pre_setconfigs) on_hook("daemon_pre_setattributes", self.pre_setattributes) on_hook("daemon_exit", self.cmd_syncto) self.reset() # sync will grab files, check the timediff on the file if the file is # actually newer (like we failed to sync last time) then it will set # fresh_config and do a syncto. self.sync_ts = 0 if (INITIAL_SYNC == 0): self.cmd_sync() elif (INITIAL_SYNC < INTERVAL): self.sync_ts = time.time() - (INTERVAL - INITIAL_SYNC)
def __init__(self, tag, id, callbacks): PluginHandler.__init__(self) self.callbacks = callbacks self.parent_tag = tag self.is_tag = False self.is_dead = False self.id = id self.pad = None self.selected = False self.marked = False # Are there changes pending? self.changed = True self.fresh_state = False self.fresh_tags = False self.width = 0 # This is used by the rendering code. self.extra_lines = 0 # Pre and post formats, to be used by plugins self.pre_format = "" self.post_format = "" # Offset globally and in-tag. self.offset = 0 self.rel_offset = 0 self.enumerated = False self.rel_enumerated = False # This should exist before the hook is setup, or the hook will fail. self.content = {} on_hook("curses_opt_change", self.on_opt_change, self) on_hook("curses_tag_opt_change", self.on_tag_opt_change, self) on_hook("curses_attributes", self.on_attributes, self) # Grab initial content, if any, the rest will be handled by the # attributes hook self.content = tag_updater.get_attributes(self.id) self.new_content = None self.plugin_class = StoryPlugin self.update_plugin_lookups()
def __init__(self): self.color_conf = config.get_opt("color") self.style_conf = config.get_opt("style") on_hook("curses_opt_change", self.on_opt_change, self)
def __init__(self, remote): self.plugin_attrs = {"cmd_sync": self.cmd_sync} self.remote = remote on_hook("remote_print_commands", self.print_sync_commands)
def __init__(self, gui): self.plugin_attrs = {} self.gui = gui on_hook("curses_start", self.do_cmds)
offset = 2 while (gtitle + " (%d)" % offset) in names: offset += 1 gtitle = gtitle + " (%d)" % offset attrs = { "url" : gurl, "name" : gtitle } new_feeds.append(attrs) names.append(gtitle) call_hook("set_configs", [ None, { "feeds" : new_feeds }]) for curl in curls[:]: if curl not in gurls: reader.subscribe('feed/' + curl) on_hook("serving", sync_subscriptions) auth = ClientAuthMethod(USERNAME, PASSWORD) reader_lock = Lock() reader = GoogleReader(auth) def lock_reader(fn): def lock_wrap(*args, **kwargs): reader_lock.acquire() try: r = fn(*args, **kwargs) except: log.error("FAILURE") log.error(traceback.format_exc()) reader_lock.release()
log.warn("Unknown file to sync: %s" % target) return log.debug("Syncto cmd: %s", cmd) try: out = subprocess.check_output(cmd) except Exception as e: log.warn("Command %s : %s" % (cmd, e)) else: log.debug("Syncto output: %s", out) def rsync_from(target, fname): if target in targets: cmd = CMD + [ SYNC_LOCATION + targets[target], fname ] else: log.warn("Unknown file to sync: %s" % target) return log.debug("Syncfrom cmd: %s", cmd) try: out = subprocess.check_output(cmd) except Exception as e: log.warn("Command %s : %s" % (cmd, e)) else: log.debug("Syncfrom output: %s", out) on_hook("daemon_syncfrom", rsync_from) on_hook("daemon_syncto", rsync_to)
def __init__(self, callbacks, types = [InputBox, TagList]): CommandHandler.__init__(self) self.plugin_class = ScreenPlugin self.update_plugin_lookups() self.callbacks = callbacks self.layout = "default" self.window_types = types self.stdscr = curses.initscr() if self.curses_setup() < 0: return -1 self.pseudo_input_box = curses.newpad(1,1) self.pseudo_input_box.keypad(1) self.pseudo_input_box.nodelay(1) self.input_lock = Lock() set_redisplay_callback(self.readline_redisplay) set_getc(self.readline_getc) # See Python bug 2675, readline + curses os.unsetenv('LINES') os.unsetenv('COLUMNS') self.floats = [] self.tiles = [] self.windows = [] self.subwindows() args = { "color_name" : ("[color name] Either a pair number (0-255, >8 ignored on 8 color terminals), a default fore/background (deffg, defbg), or an arbitrary name to be used in themes (unread, pending, etc.)", self.type_color_name), "fg-color" : ("[fg-color] Foreground color", self.type_color), "bg-color" : ("[bg-color] Background color (optional)\n\nNamed colors: white black red yellow green blue magenta pink\nNumeric colors: 1-256", self.type_color), "style" : ("[style] Curses style (normal, bold, dim, reverse, standout, underline)", self.type_style), } cmds = { "color": (self.cmd_color, ["color_name", "fg-color", "bg-color"], """Change the color palette. Most like you want to use this to change a color used in the theme. For example, :color unread green Will change the color of unread items to green, with the default background. The list of names used in the default theme are: unread read selected marked pending error reader_quote reader_link reader_image_link reader_italics enum_hints You can also change the defaults :color deffg blue :color defbg white Which will be used anywhere a color pair doesn't have an explicit foreground/background. Lastly you can change the color pairs themselves. This isn't recommended, they're initialized so each available color is available with the default background. If you change these pairs, the named colors above may not make any sense (i.e. green really turns on the color pair set aside for green, so if you change that pair to actually be yellow, don't expect this command to figure it out). :color 1 white red Arguments:""" ), "style": (self.cmd_style, ["color_name", "style"], """Change the curses style of a named color. For example, :style selected underline The names used in the default theme are: unread read selected marked pending error reader_quote reader_link reader_image_link reader_italics enum_hints Changing other colors (numeric pairs or deffg/defbg) will have no effect as these are separate from the built in curses color system. Arguments:"""), } register_arg_types(self, args) register_commands(self, cmds, "Theme") on_hook("curses_opt_change", self.screen_opt_change)
attrs = feed.get_attributes([item_id], { item_id :\ ["canto_inoreader_id", "canto_inoreader_categories", "canto-state", "canto-tags"] }) attrs = attrs[item_id] # If the canto_inoreader_id isn't right (likely empty since get_attributes # will sub in "") then skip synchronizing this item. ino_id = attrs["canto_inoreader_id"] if not ino_id.startswith("tag:google.com,2005:reader/item/"): continue sync_state_to(args[item_id], attrs) api.flush_changes() on_hook("daemon_post_setattributes", post_setattributes) def post_setconfigs(socket, args): if "feeds" in args: for feed in args["feeds"]: api.add_sub(feed["url"], feed["name"]) on_hook("daemon_post_setconfigs", post_setconfigs) def post_delconfigs(socket, args): if "feeds" in args: for feed in args["feeds"]: api.del_sub(feed["url"]) on_hook("daemon_post_delconfigs", post_delconfigs)
# Set to True if you want the selection title included. USE_TITLE=False from canto_next.hooks import on_hook import locale import os prefcode = locale.getpreferredencoding() def set_xterm_title(s): os.write(1, ("\033]0; %s \007" % s).encode(prefcode)) def clear_xterm_title(): os.write(1, "\033]0; \007".encode(prefcode)) def xt_on_var_change(var_dict): if "selected" in var_dict: if var_dict["selected"] and "title" in var_dict["selected"].content: set_xterm_title("Canto - " + var_dict["selected"].content["title"]) else: set_xterm_title("Canto") if USE_TITLE: on_hook("curses_var_change", xt_on_var_change) else: on_hook("curses_start", lambda: set_xterm_title("Canto")) on_hook("curses_exit", clear_xterm_title)
def init(self, pad, callbacks): GuiBase.init(self) # Drawing information self.pad = pad self.height, self.width = self.pad.getmaxyx() # Callback information self.callbacks = callbacks # Holster for a list of items for batch operations. self.got_items = [] self.first_sel = None self.first_story = None self.last_story = None self.tags = [] # Hold config log so we don't miss any new TagCores or get updates # before we're ready. on_hook("curses_eval_tags_changed", self.on_eval_tags_changed, self) on_hook("curses_items_added", self.on_items_added, self) on_hook("curses_items_removed", self.on_items_removed, self) on_hook("curses_tag_updated", self.on_tag_updated, self) on_hook("curses_stories_added", self.on_stories_added, self) on_hook("curses_stories_removed", self.on_stories_removed, self) on_hook("curses_opt_change", self.on_opt_change, self) on_hook("curses_new_tagcore", self.on_new_tagcore, self) on_hook("curses_del_tagcore", self.on_del_tagcore, self) args = { "cursor-offset": ("[cursor-offset]", self.type_cursor_offset), "item-list": ("[item-list]: List of item indices (tab complete to show)\n Simple: 1,3,6,5\n Ranges: 1-100\n All: *\n Selected item: .\n Domains tag,1,2,3 for 1,2,3 of current tag", self.type_item_list, self.hook_item_list), "item-state": ("[item-state]: Any word, can be inverted with minus ex: '-read' or 'marked'", self.type_item_state), "tag-list": ("[tag-list]: List of tag indices (tab complete to show)\n Simple: 1,3,6,5\n Ranges: 1-100\n Selected tag: .\n All: *", self.type_tag_list, self.hook_tag_list), # string because tag-item will manually bash in user: prefix "user-tag": ("[user-tag]: Any string, like 'favorite', or 'cool'", self.type_user_tag), "category": ("[category]: Any string, like 'news' or 'comics'", self.type_category), } base_cmds = { "remote delfeed": (self.cmd_delfeed, ["tag-list"], "Unsubscribe from feeds."), } nav_cmds = { "page-down": (self.cmd_page_down, [], "Move down a page of items"), "page-up": (self.cmd_page_up, [], "Move up a page of items"), "next-tag": (self.cmd_next_tag, [], "Scroll to next tag"), "prev-tag": (self.cmd_prev_tag, [], "Scroll to previous tag"), "next-marked": (self.cmd_next_marked, [], "Scroll to next marked item"), "prev-marked": (self.cmd_prev_marked, [], "Scroll to previous marked item"), "rel-set-cursor 1": (lambda: self.cmd_rel_set_cursor(1), [], "Next item"), "rel-set-cursor -1": (lambda: self.cmd_rel_set_cursor(-1), [], "Previous item"), } hidden_cmds = { "rel-set-cursor": (self.cmd_rel_set_cursor, ["cursor-offset"], "Move the cursor by cursor-offset items"), } grouping_cmds = { "foritems": (self.cmd_foritems, ["item-list"], "Collect items for future commands\n\nAfter a foritems call, subsequent commands that take [item-lists] will use them.\n\nCan be cleared with clearitems." ), "foritem": (self.cmd_foritem, ["item-list"], "Collect first item for future commands\n\nAfter a foritem call, subsequent commands that take [item-lists] will use the first item given.\n\nCan be cleared with clearitems." ), "clearitems": (self.cmd_clearitems, [], "Clear collected items (see foritem / foritems)"), } item_cmds = { "goto": (self.cmd_goto, ["item-list"], "Open story links in browser"), "reader": (self.cmd_reader, ["item-list"], "Open the built-in reader"), "tag-item": (self.cmd_tag_item, ["user-tag", "item-list"], "Add a tag to individual items"), "tags": (self.cmd_tags, ["item-list"], "Show tag of selected items"), "item-state": (self.cmd_item_state, ["item-state", "item-list"], "Set item state (i.e. 'item-state read .')"), "tag-state": (self.cmd_tag_state, ["item-state", "tag-list"], "Set item state for all items in tag (i.e. 'tag-state read .')"), } collapse_cmds = { "collapse": (self.cmd_collapse, ["tag-list"], "Collapse tags - reduce the tag's output to a simple status line." ), "uncollapse": (self.cmd_uncollapse, ["tag-list"], "Uncollapse tags - show the full content of a tag"), "toggle-collapse": (self.cmd_toggle_collapse, ["tag-list"], "Toggle collapsed state of tags."), } search_cmds = { "search": (self.cmd_search, ["string"], "Search items for string"), "search-regex": (self.cmd_search_regex, ["string"], "Search items for regex"), } tag_cmds = { "promote": (self.cmd_promote, ["tag-list"], "Move tags up in the display order (opposite of demote)"), "demote": (self.cmd_demote, ["tag-list"], "Move tags down in the display order (opposite of promote)"), } tag_group_cmds = { "categorize": (self.cmd_categorize, ["category", "tag-list"], "Categorize a tag"), "remove-category": (self.cmd_remove_category, ["category", "tag-list"], "Remove a tag from a category"), "categories": (self.cmd_categories, ["tag-list"], "Query what categories a tag is in."), "show-category": (self.cmd_show_category, ["category"], "Show only tags in category."), } register_commands(self, base_cmds, "Base") register_commands(self, nav_cmds, "Navigation") register_commands(self, hidden_cmds, "hidden") register_commands(self, grouping_cmds, "Grouping") register_commands(self, item_cmds, "Item") register_commands(self, collapse_cmds, "Collapse") register_commands(self, search_cmds, "Search") register_commands(self, tag_cmds, "Tag") register_commands(self, tag_group_cmds, "Tag Grouping") register_arg_types(self, args) self.plugin_class = TagListPlugin self.update_plugin_lookups()
def hook_tag_list(self): if not self.callbacks["get_opt"]("taglist.tags_enumerated"): self.callbacks["set_opt"]("taglist.tags_enumerated", True) self.callbacks["release_gui"]() on_hook("curses_var_change", self.unhook_tag_list, self)
def hook_item_list(self): if not self.callbacks["get_opt"]("story.enumerated"): self.callbacks["set_opt"]("story.enumerated", True) self.callbacks["release_gui"]() on_hook("curses_var_change", self.unhook_item_list, self)
def check(self): config_script = { 'VERSION' : { '*' : [('VERSION', CANTO_PROTOCOL_COMPATIBLE)] }, 'CONFIGS' : { '*' : [('CONFIGS', { "CantoCurses" : config.template_config })] }, } config_backend = TestBackend("config", config_script) config.init(config_backend, CANTO_PROTOCOL_COMPATIBLE) config_backend.inject("NEWTAGS", [ "maintag:Slashdot", "maintag:reddit" ]) tagcore_script = {} tag_backend = TestBackend("tagcore", tagcore_script) on_hook("curses_items_removed", self.on_items_removed) on_hook("curses_items_added", self.on_items_added) on_hook("curses_new_tagcore", self.on_new_tagcore) on_hook("curses_del_tagcore", self.on_del_tagcore) on_hook("curses_attributes", self.on_attributes) on_hook("curses_update_complete", self.on_update_complete) on_hook("curses_tag_updated", self.on_tag_updated) # 1. Previously existing tags in config should be populated on init self.reset_flags() tag_updater.init(tag_backend) for tag in config.vars["strtags"]: for tc in alltagcores: if tc.tag == tag: break else: raise Exception("Couldn't find TC for tag %s" % tag) self.compare_flags(NEW_TC) self.reset_flags() # 2. Getting empty ITEMS responses should cause no events tag_backend.inject("ITEMS", { "maintag:Slashdot" : [] }) tag_backend.inject("ITEMSDONE", {}) tag_backend.inject("ITEMS", { "maintag:reddit" : [] }) tag_backend.inject("ITEMSDONE", {}) self.compare_flags(0) # 3. Getting a non-empty ITEMS response should cause items_added tag_backend.inject("ITEMS", { "maintag:Slashdot" : [ "id1", "id2" ] }) tag_backend.inject("ITEMSDONE", {}) self.compare_flags(ITEMS_ADDED) # 4. Getting attributes should cause attributes hook self.reset_flags() id1_content = { "title" : "id1", "canto-state" : [], "canto-tags" : [], "link" : "id1-link", "enclosures" : "" } id2_content = { "title" : "id2", "canto-state" : [], "canto-tags" : [], "link" : "id2-link", "enclosures" : "" } all_content = { "id1" : id1_content, "id2" : id2_content } tag_backend.inject("ATTRIBUTES", all_content) self.compare_flags(ATTRIBUTES) self.compare_var("attributes", all_content) id1_got = tag_updater.get_attributes("id1") if id1_got != id1_content: raise Exception("Bad content: wanted %s - got %s" % (id1_content, id1_got)) id2_got = tag_updater.get_attributes("id2") if id2_got != id2_content: raise Exception("Bad content: wanted %s - got %s" % (id2_content, id2_got)) # 5. Removing an item should *NOT* cause its attributes to be forgotten # that happens on stories_removed, and should cause ITEMS_REMOVED self.reset_flags() tag_backend.inject("ITEMS", { "maintag:Slashdot" : [ "id1" ] }) tag_backend.inject("ITEMSDONE", {}) self.compare_flags(ITEMS_REMOVED) id2_got = tag_updater.get_attributes("id2") if id2_got != id2_content: raise Exception("Bad content: wanted %s - got %s" % (id2_content, id2_got)) # 6. Getting a stories_removed hook should make it forget attributes self.reset_flags() call_hook("curses_stories_removed", [ FakeTag("maintag:Slashdot"), [ FakeStory("id2") ] ]) if "id2" in tag_updater.attributes: raise Exception("Expected id2 to be removed, but it isn't!") self.compare_flags(0) # 7. Getting attributes for non-existent IDs should return empty id2_got = tag_updater.get_attributes("id2") if id2_got != {}: raise Exception("Expected non-existent id to return empty! Got %s" % id2_got) self.compare_flags(0) # 8. Getting stories_removed for item still in tag should do nothing call_hook("curses_stories_removed", [ FakeTag("maintag:Slashdot"), [ FakeStory("id1") ] ]) if "id1" not in tag_updater.attributes: raise Exception("Expected id1 to remain in attributes!") self.compare_flags(0) # 9. Config adding a tag should create a new tagcore config_backend.inject("NEWTAGS", [ "maintag:Test1" ]) self.compare_flags(NEW_TC) # 10. Config removing an empty tag should delete a tagcore self.reset_flags() config_backend.inject("DELTAGS", [ "maintag:reddit" ]) self.compare_flags(DEL_TC) self.compare_var("del_tc", "maintag:reddit") # 11. Config removing an populated tag should delete a tagcore and # cause items_removed. NOTE for now tagcores are never deleted, they # just exist empty self.reset_flags() config_backend.inject("DELTAGS", [ "maintag:Slashdot" ]) self.compare_flags(DEL_TC | ITEMS_REMOVED) self.compare_var("del_tc", "maintag:Slashdot") self.compare_var("oir_tctag", "maintag:Slashdot") self.compare_var("oir_tcids", [ "id1" ]) # 12. Update should cause all tags to generate a tag_update # hook call on ITEMS, and update_complete when all done. self.reset_flags() tag_updater.update() tag_backend.inject("ITEMS", { "maintag:Test1" : [ "id3", "id4" ] }) tag_backend.inject("ITEMSDONE", {}) tag_backend.inject("ATTRIBUTES", { "id3" : { "test" : "test" }, "id4" : { "test" : "test" }}) print(tag_updater.updating) self.compare_flags(TAG_UPDATED | UPDATE_COMPLETE | ITEMS_ADDED | ATTRIBUTES) self.compare_var("otu_tag", "maintag:Test1") return True
def __init__(self, tagcore, callbacks): list.__init__(self) PluginHandler.__init__(self) self.tagcore = tagcore self.tag = tagcore.tag self.is_tag = True self.updates_pending = 0 self.pad = None self.footpad = None # Note that Tag() is only given the top-level CantoCursesGui # callbacks as it shouldn't be doing input / refreshing # itself. self.callbacks = callbacks.copy() # Modify our own callbacks so that *_tag_opt assumes # the current tag. self.callbacks["get_tag_opt"] =\ lambda x : callbacks["get_tag_opt"](self.tag, x) self.callbacks["set_tag_opt"] =\ lambda x, y : callbacks["set_tag_opt"](self.tag, x, y) self.callbacks["get_tag_name"] = lambda: self.tag # This could be implemented as a generic, top-level hook but then N # tags would have access to story objects they shouldn't have and # would have to check every items membership in self, which would be # pointless and time-consuming. self.callbacks["item_state_change"] =\ self.on_item_state_change # Are there changes pending? self.changed = True self.selected = False self.marked = False # Information from last refresh self.footlines = 0 self.extra_lines = 0 self.width = 0 self.collapsed = False self.border = False self.enumerated = False self.abs_enumerated = False # Formats for plugins to override self.pre_format = "" self.post_format = "" # Global indices (for enumeration) self.item_offset = -1 self.visible_tag_offset = -1 self.tag_offset = -1 self.sel_offset = -1 on_hook("curses_opt_change", self.on_opt_change, self) on_hook("curses_tag_opt_change", self.on_tag_opt_change, self) on_hook("curses_attributes", self.on_attributes, self) on_hook("curses_items_added", self.on_items_added, self) # Upon creation, this Tag adds itself to the # list of all tags. alltags.append(self) self.plugin_class = TagPlugin self.update_plugin_lookups()
def update_text(self): reader_conf = self.callbacks["get_opt"]("reader") s = "No selected story.\n" extra_content = "" sel = self.callbacks["get_var"]("reader_item") if sel: self.links = [("link", sel.content["link"], "mainlink")] s = "%B" + prep_for_display(sel.content["title"]) + "%b\n" # Make sure the story has the most recent info before we check it. sel.sync() # We use the description for most reader content, so if it hasn't # been fetched yet then grab that from the server now and setup # a hook to get notified when sel's attributes are changed. l = [ "description", "content", "links", "media_content", "enclosures" ] for attr in l: if attr not in sel.content: tag_updater.request_attributes(sel.id, l) s += "%BWaiting for content...%b\n" on_hook("curses_attributes", self.on_attributes, self) break else: # Grab text content over description, as it's likely got more # information. mainbody = sel.content["description"] if "content" in sel.content: for c in sel.content["content"]: if "type" in c and "text" in c["type"]: mainbody = c["value"] # Add enclosures before HTML parsing so that we can add a link # and have the remaining link logic pick it up as normal. if reader_conf['show_enclosures']: parsed_enclosures = [] if sel.content["links"]: for lnk in sel.content["links"]: if 'rel' in lnk and 'href' in lnk and lnk[ 'rel'] == 'enclosure': if 'type' not in lnk: lnk['type'] = 'unknown' parsed_enclosures.append( (lnk['href'], lnk['type'])) if sel.content["media_content"] and 'href' in sel.content[ "media_content"]: if 'type' not in sel.content["media_content"]: sel.content['media_content']['type'] = 'unknown' parsed_enclosures.append((sel.content["media_content"]['href'],\ sel.content["media_content"]['type'])) if sel.content["enclosures"] and 'href' in sel.content[ "enclosures"]: if 'type' not in sel.content["enclosures"]: sel.content['enclosures']['type'] = 'unknown' parsed_enclosures.append((sel.content['enclosures']['href'],\ sel.content['enclosures']['type'])) if not parsed_enclosures: mainbody += "<br />[ No enclosures. ]<br />" else: for lnk, typ in parsed_enclosures: mainbody += "<a href=\"" mainbody += lnk mainbody += "\">[" mainbody += typ mainbody += "]</a>\n" for attr in list(self.plugin_attrs.keys()): if not attr.startswith("edit_"): continue try: a = getattr(self, attr) (mainbody, extra_content) = a(mainbody, extra_content) except: log.error("Error running Reader edit plugin") log.error(traceback.format_exc()) # This needn't be prep_for_display'd because the HTML parser # handles that. content, links = htmlparser.convert(mainbody) # 0 always is the mainlink, append other links # to the list. self.links += links if reader_conf['show_description']: s += self.quote_rgx.sub( cc("reader_quote") + "\"\\1\"" + cc.end("reader_quote"), content) if reader_conf['enumerate_links']: s += "\n\n" for idx, (t, url, text) in enumerate(self.links): text = prep_for_display(text) url = prep_for_display(url) link_text = "[%B" + str(idx) + "%b][" +\ text + "]: " + url + "\n\n" if t == "link": link_text = cc("reader_link") + link_text + cc.end( "reader_link") elif t == "image": link_text = cc( "reader_image_link") + link_text + cc.end( "reader_image_link") s += link_text # After we have generated the entirety of the content, # strip out any egregious spacing. self.text = s.rstrip(" \t\v\n") + extra_content
def __init__(self, remote): self.plugin_attrs = { "cmd_sync" : self.cmd_sync } self.remote = remote on_hook("remote_print_commands", self.print_sync_commands)
def init(self, pad, callbacks): GuiBase.init(self) # Drawing information self.pad = pad self.height, self.width = self.pad.getmaxyx() # Callback information self.callbacks = callbacks # Holster for a list of items for batch operations. self.got_items = [] self.first_sel = None self.first_story = None self.last_story = None self.tags = [] # Hold config log so we don't miss any new TagCores or get updates # before we're ready. on_hook("curses_eval_tags_changed", self.on_eval_tags_changed, self) on_hook("curses_items_added", self.on_items_added, self) on_hook("curses_items_removed", self.on_items_removed, self) on_hook("curses_tag_updated", self.on_tag_updated, self) on_hook("curses_stories_added", self.on_stories_added, self) on_hook("curses_stories_removed", self.on_stories_removed, self) on_hook("curses_opt_change", self.on_opt_change, self) on_hook("curses_new_tagcore", self.on_new_tagcore, self) on_hook("curses_del_tagcore", self.on_del_tagcore, self) args = { "cursor-offset": ("[cursor-offset]", self.type_cursor_offset), "item-list": ("[item-list]: List of item indices (tab complete to show)\n Simple: 1,3,6,5\n Ranges: 1-100\n All: *\n Selected item: .\n Domains tag,1,2,3 for 1,2,3 of current tag", self.type_item_list, self.hook_item_list), "item-state": ("[item-state]: Any word, can be inverted with minus ex: '-read' or 'marked'", self.type_item_state), "tag-list": ("[tag-list]: List of tag indices (tab complete to show)\n Simple: 1,3,6,5\n Ranges: 1-100\n Selected tag: .\n All: *", self.type_tag_list, self.hook_tag_list), # string because tag-item will manually bash in user: prefix "user-tag" : ("[user-tag]: Any string, like 'favorite', or 'cool'", self.type_user_tag), "category" : ("[category]: Any string, like 'news' or 'comics'", self.type_category), } base_cmds = { "remote delfeed" : (self.cmd_delfeed, ["tag-list"], "Unsubscribe from feeds."), } nav_cmds = { "page-down": (self.cmd_page_down, [], "Move down a page of items"), "page-up": (self.cmd_page_up, [], "Move up a page of items"), "next-tag" : (self.cmd_next_tag, [], "Scroll to next tag"), "prev-tag" : (self.cmd_prev_tag, [], "Scroll to previous tag"), "next-marked" : (self.cmd_next_marked, [], "Scroll to next marked item"), "prev-marked" : (self.cmd_prev_marked, [], "Scroll to previous marked item"), "rel-set-cursor 1": (lambda : self.cmd_rel_set_cursor(1), [], "Next item"), "rel-set-cursor -1": (lambda : self.cmd_rel_set_cursor(-1), [], "Previous item"), } hidden_cmds = { "rel-set-cursor": (self.cmd_rel_set_cursor, ["cursor-offset"], "Move the cursor by cursor-offset items"), } grouping_cmds = { "foritems": (self.cmd_foritems, ["item-list"], "Collect items for future commands\n\nAfter a foritems call, subsequent commands that take [item-lists] will use them.\n\nCan be cleared with clearitems."), "foritem": (self.cmd_foritem, ["item-list"], "Collect first item for future commands\n\nAfter a foritem call, subsequent commands that take [item-lists] will use the first item given.\n\nCan be cleared with clearitems."), "clearitems": (self.cmd_clearitems, [], "Clear collected items (see foritem / foritems)"), } item_cmds = { "goto": (self.cmd_goto, ["item-list"], "Open story links in browser"), "reader": (self.cmd_reader, ["item-list"], "Open the built-in reader"), "tag-item" : (self.cmd_tag_item, ["user-tag", "item-list"], "Add a tag to individual items"), "tags": (self.cmd_tags, ["item-list"], "Show tag of selected items"), "item-state": (self.cmd_item_state, ["item-state", "item-list"], "Set item state (i.e. 'item-state read .')"), "tag-state": (self.cmd_tag_state, ["item-state", "tag-list"], "Set item state for all items in tag (i.e. 'tag-state read .')"), } collapse_cmds = { "collapse" : (self.cmd_collapse, ["tag-list"], "Collapse tags - reduce the tag's output to a simple status line."), "uncollapse" : (self.cmd_uncollapse, ["tag-list"], "Uncollapse tags - show the full content of a tag"), "toggle-collapse" : (self.cmd_toggle_collapse, ["tag-list"], "Toggle collapsed state of tags."), } search_cmds = { "search" : (self.cmd_search, ["string"], "Search items for string"), "search-regex" : (self.cmd_search_regex, ["string"], "Search items for regex"), } tag_cmds = { "promote" : (self.cmd_promote, ["tag-list"], "Move tags up in the display order (opposite of demote)"), "demote" : (self.cmd_demote, ["tag-list"], "Move tags down in the display order (opposite of promote)"), } tag_group_cmds = { "categorize" : (self.cmd_categorize, ["category", "tag-list"], "Categorize a tag"), "remove-category" : (self.cmd_remove_category, ["category", "tag-list"], "Remove a tag from a category"), "categories" : (self.cmd_categories, ["tag-list"], "Query what categories a tag is in."), "show-category" : (self.cmd_show_category, ["category"], "Show only tags in category."), } register_commands(self, base_cmds, "Base") register_commands(self, nav_cmds, "Navigation") register_commands(self, hidden_cmds, "hidden") register_commands(self, grouping_cmds, "Grouping") register_commands(self, item_cmds, "Item") register_commands(self, collapse_cmds, "Collapse") register_commands(self, search_cmds, "Search") register_commands(self, tag_cmds, "Tag") register_commands(self, tag_group_cmds, "Tag Grouping") register_arg_types(self, args) self.plugin_class = TagListPlugin self.update_plugin_lookups()
def update_text(self): reader_conf = self.callbacks["get_opt"]("reader") s = "No selected story.\n" sel = self.callbacks["get_var"]("reader_item") if sel: self.links = [("link",sel.content["link"],"mainlink")] s = "%1%B" + prep_for_display(sel.content["title"]) + "%b\n" # We use the description for most reader content, so if it hasn't # been fetched yet then grab that from the server now and setup # a hook to get notified when sel's attributes are changed. if "description" not in sel.content\ and "content" not in sel.content: self.callbacks["prio_write"]("ATTRIBUTES",\ { sel.id : ["description", "content"] }) s += "%BWaiting for content...%b\n" on_hook("curses_attributes", self.on_attributes) else: # Add enclosures before HTML parsing so that we can add a link # and have the remaining link logic pick it up as normal. extra_content = "" if reader_conf['show_enclosures']: for enc in sel.content["enclosures"]: # No point in enclosures without links if "href" not in enc: continue if "type" not in enc: enc["type"] = "unknown" if not extra_content: extra_content = "\n\n" extra_content += "<a href=\"" extra_content += enc["href"] extra_content += "\">(" extra_content += enc["type"] extra_content += ")</a>\n" # Grab text content over description, as it's likely got more # information. mainbody = sel.content["description"] if "content" in sel.content: for c in sel.content["content"]: if "type" in c and "text" in c["type"]: mainbody = c["value"] # This needn't be prep_for_display'd because the HTML parser # handles that. content, links = htmlparser.convert(mainbody + extra_content) # 0 always is the mainlink, append other links # to the list. self.links += links if reader_conf['show_description']: s += self.quote_rgx.sub("%6\"\\1\"%0", content) if reader_conf['enumerate_links']: s += "\n\n" for idx, (t, url, text) in enumerate(self.links): text = prep_for_display(text) url = prep_for_display(url) link_text = "[%B" + str(idx) + "%b][" +\ text + "]: " + url + "\n\n" if t == "link": link_text = "%5" + link_text + "%0" elif t == "image": link_text = "%4" + link_text + "%0" s += link_text # After we have generated the entirety of the content, # strip out any egregious spacing. self.text = s.rstrip(" \t\v\n")
def __init__(self, callbacks, types=[InputBox, TagList]): CommandHandler.__init__(self) self.plugin_class = ScreenPlugin self.update_plugin_lookups() self.callbacks = callbacks self.layout = "default" self.window_types = types self.stdscr = curses.initscr() if self.curses_setup() < 0: return -1 self.pseudo_input_box = curses.newpad(1, 1) self.pseudo_input_box.keypad(1) self.pseudo_input_box.nodelay(1) self.input_lock = Lock() set_redisplay_callback(self.readline_redisplay) set_getc(self.readline_getc) # See Python bug 2675, readline + curses os.unsetenv('LINES') os.unsetenv('COLUMNS') self.floats = [] self.tiles = [] self.windows = [] self.subwindows() args = { "color_name": ("[color name] Either a pair number (0-255, >8 ignored on 8 color terminals), a default fore/background (deffg, defbg), or an arbitrary name to be used in themes (unread, pending, etc.)", self.type_color_name), "fg-color": ("[fg-color] Foreground color", self.type_color), "bg-color": ("[bg-color] Background color (optional)\n\nNamed colors: white black red yellow green blue magenta pink\nNumeric colors: 1-256", self.type_color), "style": ("[style] Curses style (normal, bold, dim, reverse, standout, underline)", self.type_style), } cmds = { "color": (self.cmd_color, ["color_name", "fg-color", "bg-color"], """Change the color palette. Most like you want to use this to change a color used in the theme. For example, :color unread green Will change the color of unread items to green, with the default background. The list of names used in the default theme are: unread read selected marked pending error reader_quote reader_link reader_image_link reader_italics enum_hints You can also change the defaults :color deffg blue :color defbg white Which will be used anywhere a color pair doesn't have an explicit foreground/background. Lastly you can change the color pairs themselves. This isn't recommended, they're initialized so each available color is available with the default background. If you change these pairs, the named colors above may not make any sense (i.e. green really turns on the color pair set aside for green, so if you change that pair to actually be yellow, don't expect this command to figure it out). :color 1 white red Arguments:"""), "style": (self.cmd_style, ["color_name", "style"], """Change the curses style of a named color. For example, :style selected underline The names used in the default theme are: unread read selected marked pending error reader_quote reader_link reader_image_link reader_italics enum_hints Changing other colors (numeric pairs or deffg/defbg) will have no effect as these are separate from the built in curses color system. Arguments:"""), } register_arg_types(self, args) register_commands(self, cmds, "Theme") on_hook("curses_opt_change", self.screen_opt_change)
def init(self, pad, callbacks): TextBox.init(self, pad, callbacks) self.quote_rgx = re.compile("[\\\"](.*?)[\\\"]") on_hook("curses_opt_change", self.on_opt_change) on_hook("curses_var_change", self.on_var_change)
log.debug("Syncto cmd: %s", cmd) try: out = subprocess.check_output(cmd) except Exception as e: log.warn("Command %s : %s" % (cmd, e)) else: log.debug("Syncto output: %s", out) def rsync_from(target, fname): if target in targets: cmd = CMD + [SYNC_LOCATION + targets[target], fname] else: log.warn("Unknown file to sync: %s" % target) return log.debug("Syncfrom cmd: %s", cmd) try: out = subprocess.check_output(cmd) except Exception as e: log.warn("Command %s : %s" % (cmd, e)) else: log.debug("Syncfrom output: %s", out) on_hook("daemon_syncfrom", rsync_from) on_hook("daemon_syncto", rsync_to)
["canto_inoreader_id", "canto_inoreader_categories", "canto-state", "canto-tags"] }) attrs = attrs[item_id] # If the canto_inoreader_id isn't right (likely empty since get_attributes # will sub in "") then skip synchronizing this item. ino_id = attrs["canto_inoreader_id"] if not ino_id.startswith("tag:google.com,2005:reader/item/"): continue sync_state_to(args[item_id], attrs) api.flush_changes() on_hook("daemon_post_setattributes", post_setattributes) def post_setconfigs(socket, args): if "feeds" in args: for feed in args["feeds"]: api.add_sub(feed["url"], feed["name"]) on_hook("daemon_post_setconfigs", post_setconfigs) def post_delconfigs(socket, args): if "feeds" in args: for feed in args["feeds"]: api.del_sub(feed["url"])
def update_text(self): reader_conf = self.callbacks["get_opt"]("reader") s = "No selected story.\n" extra_content = "" sel = self.callbacks["get_var"]("reader_item") if sel: self.links = [("link",sel.content["link"],"mainlink")] s = "%B" + prep_for_display(sel.content["title"]) + "%b\n" # Make sure the story has the most recent info before we check it. sel.sync() # We use the description for most reader content, so if it hasn't # been fetched yet then grab that from the server now and setup # a hook to get notified when sel's attributes are changed. l = ["description", "content", "links", "media_content", "enclosures"] for attr in l: if attr not in sel.content: tag_updater.request_attributes(sel.id, l) s += "%BWaiting for content...%b\n" on_hook("curses_attributes", self.on_attributes, self) break else: # Grab text content over description, as it's likely got more # information. mainbody = sel.content["description"] if "content" in sel.content: for c in sel.content["content"]: if "type" in c and "text" in c["type"]: mainbody = c["value"] # Add enclosures before HTML parsing so that we can add a link # and have the remaining link logic pick it up as normal. if reader_conf['show_enclosures']: parsed_enclosures = [] if sel.content["links"]: for lnk in sel.content["links"]: if 'rel' in lnk and 'href' in lnk and lnk['rel'] == 'enclosure': if 'type' not in lnk: lnk['type'] = 'unknown' parsed_enclosures.append((lnk['href'], lnk['type'])) if sel.content["media_content"] and 'href' in sel.content["media_content"]: if 'type' not in sel.content["media_content"]: sel.content['media_content']['type'] = 'unknown' parsed_enclosures.append((sel.content["media_content"]['href'],\ sel.content["media_content"]['type'])) if sel.content["enclosures"] and 'href' in sel.content["enclosures"]: if 'type' not in sel.content["enclosures"]: sel.content['enclosures']['type'] = 'unknown' parsed_enclosures.append((sel.content['enclosures']['href'],\ sel.content['enclosures']['type'])) if not parsed_enclosures: mainbody += "<br />[ No enclosures. ]<br />" else: for lnk, typ in parsed_enclosures: mainbody += "<a href=\"" mainbody += lnk mainbody += "\">[" mainbody += typ mainbody += "]</a>\n" for attr in list(self.plugin_attrs.keys()): if not attr.startswith("edit_"): continue try: a = getattr(self, attr) (mainbody, extra_content) = a(mainbody, extra_content) except: log.error("Error running Reader edit plugin") log.error(traceback.format_exc()) # This needn't be prep_for_display'd because the HTML parser # handles that. content, links = htmlparser.convert(mainbody) # 0 always is the mainlink, append other links # to the list. self.links += links if reader_conf['show_description']: s += self.quote_rgx.sub(cc("reader_quote") + "\"\\1\"" + cc.end("reader_quote"), content) if reader_conf['enumerate_links']: s += "\n\n" for idx, (t, url, text) in enumerate(self.links): text = prep_for_display(text) url = prep_for_display(url) link_text = "[%B" + str(idx) + "%b][" +\ text + "]: " + url + "\n\n" if t == "link": link_text = cc("reader_link") + link_text + cc.end("reader_link") elif t == "image": link_text = cc("reader_image_link") + link_text + cc.end("reader_image_link") s += link_text # After we have generated the entirety of the content, # strip out any egregious spacing. self.text = s.rstrip(" \t\v\n") + extra_content
def check(self): config_script = { 'VERSION': { '*': [('VERSION', CANTO_PROTOCOL_COMPATIBLE)] }, 'CONFIGS': { '*': [('CONFIGS', { "CantoCurses": config.template_config })] }, } config_backend = TestBackend("config", config_script) config.init(config_backend, CANTO_PROTOCOL_COMPATIBLE) config_backend.inject("NEWTAGS", ["maintag:Slashdot", "maintag:reddit"]) tagcore_script = {} tag_backend = TestBackend("tagcore", tagcore_script) on_hook("curses_items_removed", self.on_items_removed) on_hook("curses_items_added", self.on_items_added) on_hook("curses_new_tagcore", self.on_new_tagcore) on_hook("curses_del_tagcore", self.on_del_tagcore) on_hook("curses_attributes", self.on_attributes) on_hook("curses_update_complete", self.on_update_complete) on_hook("curses_tag_updated", self.on_tag_updated) # 1. Previously existing tags in config should be populated on init self.reset_flags() tag_updater.init(tag_backend) for tag in config.vars["strtags"]: for tc in alltagcores: if tc.tag == tag: break else: raise Exception("Couldn't find TC for tag %s" % tag) self.compare_flags(NEW_TC) self.reset_flags() # 2. Getting empty ITEMS responses should cause no events tag_backend.inject("ITEMS", {"maintag:Slashdot": []}) tag_backend.inject("ITEMSDONE", {}) tag_backend.inject("ITEMS", {"maintag:reddit": []}) tag_backend.inject("ITEMSDONE", {}) self.compare_flags(0) # 3. Getting a non-empty ITEMS response should cause items_added tag_backend.inject("ITEMS", {"maintag:Slashdot": ["id1", "id2"]}) tag_backend.inject("ITEMSDONE", {}) self.compare_flags(ITEMS_ADDED) # 4. Getting attributes should cause attributes hook self.reset_flags() id1_content = { "title": "id1", "canto-state": [], "canto-tags": [], "link": "id1-link", "enclosures": "" } id2_content = { "title": "id2", "canto-state": [], "canto-tags": [], "link": "id2-link", "enclosures": "" } all_content = {"id1": id1_content, "id2": id2_content} tag_backend.inject("ATTRIBUTES", all_content) self.compare_flags(ATTRIBUTES) self.compare_var("attributes", all_content) id1_got = tag_updater.get_attributes("id1") if id1_got != id1_content: raise Exception("Bad content: wanted %s - got %s" % (id1_content, id1_got)) id2_got = tag_updater.get_attributes("id2") if id2_got != id2_content: raise Exception("Bad content: wanted %s - got %s" % (id2_content, id2_got)) # 5. Removing an item should *NOT* cause its attributes to be forgotten # that happens on stories_removed, and should cause ITEMS_REMOVED self.reset_flags() tag_backend.inject("ITEMS", {"maintag:Slashdot": ["id1"]}) tag_backend.inject("ITEMSDONE", {}) self.compare_flags(ITEMS_REMOVED) id2_got = tag_updater.get_attributes("id2") if id2_got != id2_content: raise Exception("Bad content: wanted %s - got %s" % (id2_content, id2_got)) # 6. Getting a stories_removed hook should make it forget attributes self.reset_flags() call_hook("curses_stories_removed", [FakeTag("maintag:Slashdot"), [FakeStory("id2")]]) if "id2" in tag_updater.attributes: raise Exception("Expected id2 to be removed, but it isn't!") self.compare_flags(0) # 7. Getting attributes for non-existent IDs should return empty id2_got = tag_updater.get_attributes("id2") if id2_got != {}: raise Exception( "Expected non-existent id to return empty! Got %s" % id2_got) self.compare_flags(0) # 8. Getting stories_removed for item still in tag should do nothing call_hook("curses_stories_removed", [FakeTag("maintag:Slashdot"), [FakeStory("id1")]]) if "id1" not in tag_updater.attributes: raise Exception("Expected id1 to remain in attributes!") self.compare_flags(0) # 9. Config adding a tag should create a new tagcore config_backend.inject("NEWTAGS", ["maintag:Test1"]) self.compare_flags(NEW_TC) # 10. Config removing an empty tag should delete a tagcore self.reset_flags() config_backend.inject("DELTAGS", ["maintag:reddit"]) self.compare_flags(DEL_TC) self.compare_var("del_tc", "maintag:reddit") # 11. Config removing an populated tag should delete a tagcore and # cause items_removed. NOTE for now tagcores are never deleted, they # just exist empty self.reset_flags() config_backend.inject("DELTAGS", ["maintag:Slashdot"]) self.compare_flags(DEL_TC | ITEMS_REMOVED) self.compare_var("del_tc", "maintag:Slashdot") self.compare_var("oir_tctag", "maintag:Slashdot") self.compare_var("oir_tcids", ["id1"]) # 12. Update should cause all tags to generate a tag_update # hook call on ITEMS, and update_complete when all done. self.reset_flags() tag_updater.update() tag_backend.inject("ITEMS", {"maintag:Test1": ["id3", "id4"]}) tag_backend.inject("ITEMSDONE", {}) tag_backend.inject("ATTRIBUTES", { "id3": { "test": "test" }, "id4": { "test": "test" } }) print(tag_updater.updating) self.compare_flags(TAG_UPDATED | UPDATE_COMPLETE | ITEMS_ADDED | ATTRIBUTES) self.compare_var("otu_tag", "maintag:Test1") return True
def check(self): script = { 'VERSION': { '*': [('VERSION', CANTO_PROTOCOL_COMPATIBLE)] }, 'CONFIGS': { '*': [('CONFIGS', { "CantoCurses": config.template_config })] }, } backend = TestBackend("config", script) config.init(backend, CANTO_PROTOCOL_COMPATIBLE) on_hook("curses_tag_opt_change", self.on_tag_opt_change) on_hook("curses_opt_change", self.on_opt_change) on_hook("curses_def_opt_change", self.on_def_opt_change) on_hook("curses_feed_opt_change", self.on_feed_opt_change) on_hook("curses_new_tag", self.on_new_tag) on_hook("curses_del_tag", self.on_del_tag) on_hook("curses_eval_tags_changed", self.on_eval_tags_changed) # 1. Only Opt_change self.reset_flags() test_config = eval(repr(config.template_config)) test_config["browser"]["path"] = "testoption" backend.inject("CONFIGS", {"CantoCurses": test_config}) self.compare_flags(OPT_CHANGE) self.compare_config(config.config, "browser.path", "testoption") # Check that the opt change hook got the smallest possible changeset self.compare_var("oc_opts", {"browser": {"path": "testoption"}}) # 2. Invalid Tag_opt_change self.reset_flags() test_config = { "tags": { "test": eval(repr(config.tag_template_config)) } } backend.inject("CONFIGS", test_config) self.compare_flags(0) # 3. NEWTAG (also causes OPT_CHANGE because of tag order being # expanded) Does not cause an EVAL CHANGE because test1 doesn't get # match by the tags setting (i.e. tags starting with maintag:) self.reset_flags() backend.inject("NEWTAGS", ["test1"]) self.compare_flags(NEW_TAG | OPT_CHANGE) self.compare_config(config.config, "tagorder", ["test1"]) self.compare_config(config.vars, "curtags", []) self.compare_var("new_tags", "test1") self.compare_var("oc_opts", {"tagorder": ["test1"]}) # 4. NEWTAG, this time with EVAL because "maintag:Slashdot" does match tags self.reset_flags() backend.inject("NEWTAGS", ["maintag:Slashdot"]) self.compare_flags(NEW_TAG | OPT_CHANGE | EVAL_TAGS) self.compare_config(config.config, "tagorder", ["test1", "maintag:Slashdot"]) self.compare_config(config.vars, "curtags", ["maintag:Slashdot"]) # 5. switch_tags (promote demote) self.reset_flags() # These are fodder backend.inject("NEWTAGS", ["maintag:Test2"]) backend.inject("NEWTAGS", ["maintag:Test3"]) backend.inject("NEWTAGS", ["maintag:Test4"]) self.compare_flags(NEW_TAG | OPT_CHANGE | EVAL_TAGS) self.compare_config(config.config, "tagorder", [ "test1", "maintag:Slashdot", "maintag:Test2", "maintag:Test3", "maintag:Test4" ]) self.compare_config(config.vars, "curtags", [ "maintag:Slashdot", "maintag:Test2", "maintag:Test3", "maintag:Test4" ]) self.reset_flags() config.switch_tags("maintag:Test2", "maintag:Test3") self.compare_flags(OPT_CHANGE | EVAL_TAGS) self.compare_config(config.config, "tagorder", [ "test1", "maintag:Slashdot", "maintag:Test3", "maintag:Test2", "maintag:Test4" ]) self.compare_config(config.vars, "curtags", [ "maintag:Slashdot", "maintag:Test3", "maintag:Test2", "maintag:Test4" ]) self.compare_var( "oc_opts", { "tagorder": [ "test1", "maintag:Slashdot", "maintag:Test3", "maintag:Test2", "maintag:Test4" ] }) # 6. DELTAG self.reset_flags() backend.inject("DELTAGS", ["maintag:Test4"]) self.compare_flags(DEL_TAG | OPT_CHANGE | EVAL_TAGS) self.compare_config( config.config, "tagorder", ["test1", "maintag:Slashdot", "maintag:Test3", "maintag:Test2"]) self.compare_config( config.vars, "curtags", ["maintag:Slashdot", "maintag:Test3", "maintag:Test2"]) self.compare_var("del_tags", "maintag:Test4") # 7. Changing the tags regex self.reset_flags() # More fodder backend.inject("NEWTAGS", ["alt:t1"]) backend.inject("NEWTAGS", ["alt:t2"]) backend.inject("NEWTAGS", ["alt:t3"]) self.compare_flags(NEW_TAG | OPT_CHANGE) self.compare_config(config.config, "tagorder", [ "test1", "maintag:Slashdot", "maintag:Test3", "maintag:Test2", "alt:t1", "alt:t2", "alt:t3" ]) self.compare_config( config.vars, "curtags", ["maintag:Slashdot", "maintag:Test3", "maintag:Test2"]) self.reset_flags() c = config.get_conf() c["tags"] = "alt:.*" config.set_conf(c) self.compare_flags(OPT_CHANGE | EVAL_TAGS) return True