Example #1
0
    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()
Example #2
0
    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)
Example #3
0
    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)
Example #4
0
    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)
Example #5
0
    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)
Example #6
0
    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)
Example #7
0
    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)
Example #8
0
    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)
Example #9
0
    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()
Example #10
0
    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
Example #11
0
    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)
Example #12
0
    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)
Example #13
0
    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)
Example #14
0
    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()
Example #15
0
    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
Example #16
0
    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
Example #17
0
    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)
Example #18
0
    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()
Example #19
0
    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)
Example #20
0
    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()
Example #21
0
    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()
Example #22
0
    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)
Example #23
0
    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)
Example #24
0
    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()
Example #25
0
    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()
Example #26
0
 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)
Example #27
0
    def __init__(self, remote):
        self.plugin_attrs = {"cmd_sync": self.cmd_sync}
        self.remote = remote

        on_hook("remote_print_commands", self.print_sync_commands)
Example #28
0
    def __init__(self, gui):
        self.plugin_attrs = {}
        self.gui = gui

        on_hook("curses_start", self.do_cmds)
Example #29
0
                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()
Example #30
0
        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)
Example #31
0
    def __init__(self, gui):
        self.plugin_attrs = {}
        self.gui = gui

        on_hook("curses_start", self.do_cmds)
Example #32
0
    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)
Example #33
0
        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)
Example #34
0
# 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)
Example #35
0
    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()
Example #36
0
 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)
Example #37
0
 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
Example #39
0
    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()
Example #40
0
    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
Example #41
0
 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)
Example #42
0
    def __init__(self, remote):
        self.plugin_attrs = { "cmd_sync" : self.cmd_sync }
        self.remote = remote

        on_hook("remote_print_commands", self.print_sync_commands)
Example #43
0
 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)
Example #44
0
    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()
Example #45
0
    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")
Example #46
0
    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)
Example #47
0
    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)
Example #48
0
    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)
Example #49
0
                ["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"])
Example #50
0
    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
Example #51
0
    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
Example #52
0
 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)
Example #53
0
    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