Config setting overhaul
authorJack Miller <jack@codezen.org>
Wed, 24 Sep 2014 23:54:11 +0000 (18:54 -0500)
committerJack Miller <jack@codezen.org>
Thu, 25 Sep 2014 02:16:42 +0000 (21:16 -0500)
A lot of small changes adding up to a big change.

The big change is that you can now do

    :set [option] [value] (set a value)
    :set [option]         (display current value)

For pretty much everything in the configuration.

This works for c-c settings, as well as daemon settings, so

    :set feed.(whatever)
    :set tag.(whatever)
    :set defaults.(whatever)

will now use the selection, if necessary, to Do The Right Thing

NOTE: This is *not* intended to be the way to change most settings. Most
things, like binds, and colors, and all the various boolean toggles are
properly attached to better helpers or keybind commands. This *is*
intended to provide a way to manipulate the configuration for simple
stuff like "browser".

Now for the list of small changes

- Raw config commands (like :global_transform, :browser, and
  :txt_browser) have now been removed in favor of "set
global_transform", "set browser", "set txt_browser" which are in turn
aliases for "set defaults.global_transform" "set browser.path" and "set
browser.text" for some level of backwards compatibility.

- CantoCurses.tag settings are now CantoCurses.tagobj settings (to make
  way for tag.whatever to work intuitively.

- c-c now tracks daemon defaults and feed settings, and provides an
  interface to them, but doesn't validate them at all (that's up to the
daemon).

- :tag-config has been eliminated in favor of "set tag.whatever"

- Config settings are no longer abusing :remote

- The config module now has an 'eval_settings' list of uncompiled
  regexes. Config options matching any of these regexes are eval()'d in
an empty environment before they're set. Plugins can manipulate this
when they're first evaluated, but immediately before the curses_start
hook, they're compiled and should not be touched any longer.

canto_curses/config.py
canto_curses/gui.py
canto_curses/guibase.py
canto_curses/main.py
canto_curses/tag.py
canto_curses/taglist.py

index 8966019..ffc3ca2 100644 (file)
@@ -32,10 +32,43 @@ from threading import Thread
 import traceback
 import logging
 import curses   # Colors
-import re       # 'tags' setting is a regex
+import re
 
 log = logging.getLogger("CONFIG")
 
+# eval settings need to be somehow converted when read from input.
+
+# These are regexes so that window and color types can be handled with easy
+# wildcards, but care has to be taken.
+
+eval_settings = [\
+    "defaults\\.rate", "feed\\.rate",
+    "defaults\\.keep_time", "feed\\.keep_time",
+    "defaults\\.keep_unread", "feed\\.keep_unread",
+    "update\\.auto.enabled", "update\\.auto\\.interval",
+    "browser\\.text", "taglist\\.border",
+    "kill_daemon_on_exit",
+    ".*\\.window\\.(maxwidth|maxheight|float)",
+    "color\\..*", "tag.(enumerated|collapsed|extra_tags)",
+    "reader.(enumerate_links|show_description|show_enclosures)",
+    "taglist.(border|tags_enumerated|tags_enumerated_absolute|hide_empty_tags|search_attributes)",
+    "taglist.cursor.edge",
+    "story.(format_attrs|enumerated)"
+]
+
+# Do the one-time compile for the setting regexes. This is called after
+# plugins are evaluated, but before curses_start
+
+def finalize_eval_settings():
+    global eval_settings
+    eval_settings = [ re.compile(x) for x in eval_settings ]
+
+def needs_eval(option):
+    for reobj in eval_settings:
+        if reobj.match(option):
+            return True
+    return False
+
 class CantoCursesConfig(SubThread):
 
     # No __init__ because we want this to be global, but init must be called
@@ -82,7 +115,7 @@ class CantoCursesConfig(SubThread):
             "tags" : self.validate_tags,
             "tagorder" : self.validate_tag_order,
 
-            "tag" :
+            "tagobj" :
             {
                 "format" : self.validate_string,
                 "selected" : self.validate_string,
@@ -183,7 +216,7 @@ class CantoCursesConfig(SubThread):
             "tags" : r"maintag:.*",
             "tagorder" : [],
 
-            "tag" :
+            "tagobj" :
             {
                 "format" : DEFAULT_TAG_FSTRING,
                 "selected" : "%R",
@@ -416,6 +449,7 @@ class CantoCursesConfig(SubThread):
             "enumerated" : self.validate_bool,
             "collapsed" : self.validate_bool,
             "extra_tags" : self.validate_string_list,
+            "transform" : self.validate_string,
         }
 
         self.tag_config = {}
@@ -424,8 +458,12 @@ class CantoCursesConfig(SubThread):
             "enumerated" : False,
             "collapsed" : False,
             "extra_tags" : [],
+            "transform" : "None"
         }
 
+        self.daemon_defaults = {}
+        self.daemon_feedconf = []
+
         self.start_pthread()
 
         self.write("WATCHNEWTAGS", [])
@@ -751,6 +789,21 @@ class CantoCursesConfig(SubThread):
             if deletions and write:
                 self.write("DELCONFIGS", { "CantoCurses" : deletions })
 
+        if "defaults" in given:
+
+            # We don't honor any default settings, so just record them
+            # and pass them on to the daemon if write
+
+            self.daemon_defaults.update(given["defaults"])
+            if write:
+                self.write("SETCONFIGS", { "defaults" : self.daemon_defaults })
+
+        if "feeds" in given:
+
+            self.daemon_feedconf = given["feeds"]
+            if write:
+                self.write("SETCONFIGS", { "feeds" : self.daemon_feedconf })
+
         self.initd = True
 
     # Process new tags.
@@ -898,6 +951,24 @@ class CantoCursesConfig(SubThread):
     def set_tag_conf(self, tag, conf):
         self.prot_configs({ "tags" : { tag : conf } }, True)
 
+    def set_def_conf(self, conf):
+        self.prot_configs({ "defaults" : conf }, True)
+
+    def set_feed_conf(self, name, conf):
+        config_lock.acquire_read()
+        d_f = eval(repr(self.daemon_feedconf), {}, {})
+        config_lock.release_read()
+
+        for f in d_f:
+            if f["name"] == name:
+                log.debug("updating %s with %s" % (f, conf))
+                f.update(conf)
+                break
+        else:
+            d_f.append(conf)
+
+        self.prot_configs({ "feeds" : d_f }, True)
+
     @read_lock(config_lock)
     def get_conf(self):
         return eval(repr(self.config), {}, {})
@@ -906,7 +977,18 @@ class CantoCursesConfig(SubThread):
     def get_tag_conf(self, tag):
         if tag in self.tag_config:
             return eval(repr(self.tag_config[tag]), {}, {})
-        return eval(repr(self.tag_template_config))
+        return eval(repr(self.tag_template_config), {}, {})
+
+    @read_lock(config_lock)
+    def get_def_conf(self):
+        return eval(repr(self.daemon_defaults), {}, {})
+
+    @read_lock(config_lock)
+    def get_feed_conf(self, name):
+        for f in self.daemon_feedconf:
+            if f["name"] == name:
+                return eval(repr(f), {}, {})
+        return None
 
     @write_lock(config_lock)
     def set_opt(self, option, value):
index 83d3de6..5900d0f 100644 (file)
@@ -71,6 +71,10 @@ class CantoCursesGui(CommandHandler):
             "get_conf" : config.get_conf,
             "set_tag_conf" : config.set_tag_conf,
             "get_tag_conf" : config.get_tag_conf,
+            "set_defaults" : config.set_def_conf,
+            "get_defaults" : config.get_def_conf,
+            "set_feed_conf" : config.set_feed_conf,
+            "get_feed_conf" : config.get_feed_conf,
             "get_opt" : config.get_opt,
             "set_opt" : config.set_opt,
             "get_tag_opt" : config.get_tag_opt,
index a073653..1235f67 100644 (file)
@@ -8,10 +8,12 @@
 
 from canto_next.hooks import on_hook
 from canto_next.plugins import Plugin
+from canto_next.remote import assign_to_dict, access_dict
 
 from .command import CommandHandler, register_commands, register_arg_types, unregister_all, _string, register_aliases, commands, command_help
 from .tagcore import tag_updater
 from .parser import prep_for_display
+from .config import needs_eval
 
 import logging
 
@@ -40,6 +42,7 @@ class GuiBase(CommandHandler):
             "remote-cmd": ("[remote cmd]", self.type_remote_cmd),
             "url" : ("[URL]", _string),
             "help-command" : ("[help-command]: Any canto-curses command, if blank, 'any' or unknown, will display help overview", self.type_help_cmd),
+            "config-option" : ("[config-option]: Any canto-curses option", self.type_config_option),
         }
 
         cmds = {
@@ -49,6 +52,7 @@ class GuiBase(CommandHandler):
             "remote listfeeds" : (lambda x : self.cmd_remote("listfeeds", x), [], "List feeds"),
             "remote": (self.cmd_remote, ["remote-cmd", "string"], "Give a command to canto-remote"),
             "destroy": (self.cmd_destroy, [], "Destroy this %s" % self.get_opt_name()),
+            "set" : (self.cmd_set, ["config-option", "string"], "Set configuration options"),
         }
 
         help_cmds = {
@@ -56,42 +60,22 @@ class GuiBase(CommandHandler):
         }
 
         aliases = {
-            "browser" : "remote one-config CantoCurses.browser.path",
-            "txt_browser" : "remote one-config --eval CantoCurses.browser.text",
             "add" : "remote addfeed",
             "del" : "remote delfeed",
             "list" : "remote listfeeds",
-            "global_transform" : "remote one-config defaults.global_transform",
-            "cursor_type" : "remote one-config CantoCurses.taglist.cursor.type",
-            "cursor_scroll" : "remote one-config CantoCurses.taglist.cursor.scroll",
-            "cursor_edge" : "remote one-config --eval CantoCurses.taglist.cursor.edge",
-            "story_unselected" : "remote one-config CantoCurses.story.unselected",
-            "story_selected" : "remote one-config CantoCurses.story.selected",
-            "story_selected_end" : "remote one-config CantoCurses.story.selected_end",
-            "story_unselected_end" : "remote one-config CantoCurses.story.unselected_end",
-            "story_unread" : "remote one-config CantoCurses.story.unread",
-            "story_read" : "remote one-config CantoCurses.story.read",
-            "story_read_end" : "remote one-config CantoCurses.story.read_end",
-            "story_unread_end" : "remote one-config CantoCurses.story.unread_end",
-            "story_unmarked" : "remote one-config CantoCurses.story.unmarked",
-            "story_marked" : "remote one-config CantoCurses.story.marked",
-            "story_marked_end" : "remote one-config CantoCurses.story.marked_end",
-            "story_unmarked_end" : "remote one-config CantoCurses.story.unmarked_end",
-            "tag_unselected" : "remote one-config CantoCurses.tag.unselected",
-            "tag_selected" : "remote one-config CantoCurses.tag.selected",
-            "tag_selected_end" : "remote one-config CantoCurses.tag.selected_end",
-            "tag_unselected_end" : "remote one-config CantoCurses.tag.unselected_end",
-            "update_interval" : "remote one-config --eval CantoCurses.update.auto.interval",
-            "update_style" : "remote one-config CantoCurses.update.style",
-            "update_auto" : "remote one-config --eval CantoCurses.update.auto.enabled",
-            "border" : "remote one-config --eval CantoCurses.taglist.border",
-            "reader_align" : "remote one-config CantoCurses.reader.window.align",
-            "reader_float" : "remote one-config --eval CantoCurses.reader.window.float",
-            "keep_time" : "remote one-config --eval defaults.keep_time",
-            "keep_unread" : "remote one-config --eval defaults.keep_unread",
-            "kill_daemon_on_exit" : "remote one-config --eval CantoCurses.kill_daemon_on_exit",
+
+            # Compatibility / evaluation aliases
+            "set global_transform" : "set defaults.global_transform",
+            "set keep_time" : "set defaults.keep_time",
+            "set keep_unread" : "set defaults.keep_unread",
+            "set browser " : "set browser.path ",
+            "set txt_browser " : "set browser.text ",
+            "set update.auto " : "set update.auto.enabled ",
+            "set border" : "set taglist.border",
+
             "filter" : "transform",
             "sort" : "transform",
+
             "next-item" : "rel-set-cursor 1",
             "prev-item" : "rel-set-cursor -1",
         }
@@ -366,3 +350,101 @@ class GuiBase(CommandHandler):
                 log.info("")
         else:
             log.info(command_help(cmd, True))
+
+    # Validate a single config option
+    # Will offer completions for any recognized config option
+    # Will *not* reject validly formatted options that don't already exist
+
+    def _get_current_config_options(self, obj, stack):
+        r = []
+
+        for item in obj.keys():
+            stack.append(item)
+
+            if type(obj[item]) == dict:
+                r.extend(self._get_current_config_options(obj[item], stack[:]))
+            else:
+                r.append(shlex.quote(".".join(stack)))
+
+            stack = stack[:-1]
+
+        return r
+
+    def type_config_option(self):
+        conf = self.callbacks["get_conf"]()
+
+        possibles = self._get_current_config_options(conf, [])
+        possibles.sort()
+
+        return (possibles, lambda x : (True, x))
+
+    def cmd_set(self, opt, val):
+        log.debug("SET: %s '%s'" % (opt, val))
+
+        evaluate = needs_eval(opt)
+
+        if val != "" and evaluate:
+            log.debug("Evaluating...")
+            try:
+                val = eval(val)
+            except Exception as e:
+                log.error("Couldn't eval '%s': %s" % (val, e))
+                return
+
+        if opt.startswith("defaults."):
+            conf = { "defaults" : self.callbacks["get_defaults"]() }
+
+            if val != "":
+                assign_to_dict(conf, opt, val)
+                self.callbacks["set_defaults"](conf["defaults"])
+        elif opt.startswith("feed."):
+            sel = self.callbacks["get_var"]("selected")
+            if not sel:
+                log.info("Feed settings only work with a selected item")
+                return
+
+            if sel.is_tag:
+                try_tag = sel
+            else:
+                try_tag = sel.parent_tag
+
+            if not try_tag.tag.startswith("maintag:"):
+                log.info("Selection is in a user tag, cannot set feed settings")
+                return
+
+            name = try_tag.tag[8:]
+
+            conf = { "feed" : self.callbacks["get_feed_conf"](name) }
+
+            if val != "":
+                assign_to_dict(conf, opt, val)
+                self.callbacks["set_feed_conf"](name, conf["feed"])
+        elif opt.startswith("tag."):
+            sel = self.callbacks["get_var"]("selected")
+            if not sel:
+                log.info("Tag settings only work with a selected item")
+                return
+
+            if sel.is_tag:
+                tag = sel
+            else:
+                tag = sel.parent_tag
+
+            conf = { "tag" : self.callbacks["get_tag_conf"](tag.tag) }
+
+            if val != "":
+                assign_to_dict(conf, opt, val)
+                self.callbacks["set_tag_conf"](tag.tag, conf["tag"])
+        else:
+            conf = self.callbacks["get_conf"]()
+
+            if val != "":
+                assign_to_dict(conf, opt, val)
+                self.callbacks["set_conf"](conf)
+
+        ok, val = access_dict(conf, opt)
+        if not ok:
+            log.error("Unknown option %s" % opt)
+            log.error("Full conf: %s" % conf)
+        else:
+            log.info("%s = %s" % (opt, val))
index 329868f..ba97140 100644 (file)
@@ -11,7 +11,7 @@ from canto_next.plugins import try_plugins
 from canto_next.rwlock import alllocks
 from canto_next.hooks import call_hook
 
-from .config import config
+from .config import config, finalize_eval_settings
 from .tagcore import tag_updater, alltagcores
 from .gui import CantoCursesGui
 
@@ -207,6 +207,8 @@ class CantoCurses(CantoClient):
         signal.signal(signal.SIGWINCH, self.winch)
         signal.signal(signal.SIGCHLD, self.child)
 
+        finalize_eval_settings()
+
         call_hook("curses_start", [])
 
         while self.gui.alive:
index 904af44..2732e0d 100644 (file)
@@ -132,7 +132,7 @@ class Tag(PluginHandler, list):
                 "border" in opts["taglist"]):
             self.need_redraw()
 
-        if "tag" in opts:
+        if "tagobj" in opts:
             self.need_redraw()
 
     def on_tag_opt_change(self, opts):
@@ -266,7 +266,7 @@ class Tag(PluginHandler, list):
         if width == self.width and not self.changed:
             return self.lns
 
-        tag_conf = self.callbacks["get_opt"]("tag")
+        tag_conf = self.callbacks["get_opt"]("tagobj")
         taglist_conf = self.callbacks["get_opt"]("taglist")
 
         # Values to pass on to render
index dd0cd5c..201b1fb 100644 (file)
@@ -140,7 +140,6 @@ class TagList(GuiBase):
         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-config" : (self.cmd_tag_config, ["tag-list", "string"], "Manipulate a tag's configuration"),
         }
 
         tag_group_cmds = {
@@ -815,13 +814,6 @@ class TagList(GuiBase):
 
         self._set_cursor(cur, curpos)
 
-    def cmd_tag_config(self, tags, config):
-        for tag in tags:
-            strtag = tag.tag.replace(".","\\.")
-
-            argv = ["canto-remote", "one-config", "tags." + strtag + "." + config]
-            self._remote_argv(argv)
-
     # Just like "string" but prepend "user:"
 
     def type_category(self):