From 036db7b1a73ea71e767cfebebfa36f5c74289b4d Mon Sep 17 00:00:00 2001 From: dece Date: Sun, 27 Sep 2020 16:31:13 +0200 Subject: [PATCH] Handle basic tree import Only update existing properties. --- xfconf.py | 12 +++++++ xion.py | 96 ++++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 90 insertions(+), 18 deletions(-) diff --git a/xfconf.py b/xfconf.py index d972bdc..39ac55b 100644 --- a/xfconf.py +++ b/xfconf.py @@ -44,6 +44,18 @@ class Xfconf: return None return XfconfProperty.parse(output) + def set_property(self, channel, prop, prop_type, value): + """Create or update this property.""" + # TODO handle new properties + self.update_property(channel, prop, value) + + def update_property(self, channel, prop, value): + """Update an existing property, return True on success.""" + if " " in value: + value = f'"{value}"' + output = self.xqs(f"-c {channel} -p {prop} -s {value}") + return output == "" + @staticmethod def find_xq(): xq = shutil.which("xion-query") diff --git a/xion.py b/xion.py index 3c6511c..b329506 100644 --- a/xion.py +++ b/xion.py @@ -4,37 +4,55 @@ import json from xfconf import Xfconf -DEFAULT_FILE_PATH = "xion.json" - - def main(): argparser = argparse.ArgumentParser() - argparser.add_argument("--xq-path", type=str, - help="Optional path to xion-query") - argparser.add_argument("-e", "--export", type=str, nargs=2, - metavar=("channel", "root"), - help="Channel and root to export") - argparser.add_argument("-f", "--file", type=str, - help="JSON file for import/export") + argparser.add_argument( + "--xq-path", type=str, + help="Optional path to xion-query" + ) + argparser.add_argument( + "-e", "--export", type=str, nargs=3, + metavar=("CHANNEL", "ROOT", "OUTPUT"), + help=("Export settings in channel under this root. " + "Use '/' as root to export the whole channel.") + ) + argparser.add_argument( + "-i", "--import", dest="import_tree", type=str, + metavar=("JSON",), + help="Import a JSON settings file" + ) + argparser.add_argument( + "-y", "--yes", action="store_true", + help="Do not ask for confirmation" + ) args = argparser.parse_args() xion = Xion(xq=args.xq_path) if args.export: - channel, root = args.export + channel, root, output = args.export tree = xion.build_tree(channel, root) if tree is None: print("Failed to build config tree.") return - if args.file: - output_path = args.file - else: - print(f"No output file, using {DEFAULT_FILE_PATH}.") - output_path = DEFAULT_FILE_PATH - xion.export_tree(tree, output_path) + xion.export_tree(channel, root, tree, output) + elif args.import_tree: + channel, root, tree = xion.import_tree(args.import_tree) + if channel and root and tree: + force = bool(args.yes) + xion.apply_tree(channel, root, tree, confirm=not force) class Xion: + # GTypes to xfconf-query types along with a value string parser. + TYPE_MAP = { + "gboolean": "bool", + "gint": "int", + "guint": "uint", + "gdouble": "double", + "gchararray": "string", + } + def __init__(self, xq=None): self.xfconf = Xfconf(xq=xq) @@ -43,6 +61,9 @@ class Xion: Return None on error. """ + if not root.startswith("/"): + print("Invalid root, must start with /") + return None props = self.xfconf.get_property_list(channel, root=root) if props is None: print(f"Failed to get property list for channel {channel}.") @@ -60,11 +81,50 @@ class Xion: tree[prop_name] = leaf return tree - def export_tree(self, tree, output_path): + def export_tree(self, channel, root, tree, output_path): """Export a config tree as a sorted JSON file.""" + tree["channel"] = channel + tree["root"] = root with open(output_path, "wt") as output_file: json.dump(tree, output_file, indent=2, sort_keys=True) + def import_tree(self, file_path): + """Load a config tree.""" + with open(file_path, "rt") as input_file: + tree = json.load(input_file) + try: + channel = tree.pop("channel") + root = tree.pop("root") + except KeyError: + print("Missing channel or root in JSON.") + return None, None, tree + return channel, root, tree + + def apply_tree(self, channel, root, tree, confirm=True, replace=False): + """Apply tree settings under root to channel.""" + num_changes = len(tree) + print(f"Applying {num_changes} changes to {channel} under {root}.") + if replace: + print("This will erase all settings in the channel.") + if confirm and input("Confirm? [y/N]") != "y": + print("Operation cancelled.") + return + for prop, content in tree.items(): + self.apply_property(channel, prop, content) + + def apply_property(self, channel, name, content): + """Update one property in Xfconf, return True on success.""" + # if isinstance(content, list): + # for subprop in content: + # if not self.apply_property(channel, + prop_type = content["type"] + if not prop_type in Xion.TYPE_MAP: + print(f"Unknown property type {prop_type}!") + return False + xq_type = Xion.TYPE_MAP[prop_type] + value = content["value"] + self.xfconf.set_property(channel, name, xq_type, value) + @staticmethod def _build_prop_leaf(prop): return {"type": prop.gtype, "value": str(prop.value)}