diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py index b0636bc0..45b4ac56 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -33,6 +33,8 @@ from Xlib import X from Xlib.display import Display from Xlib.ext import record from Xlib.protocol import rq +from yaml import add_representer as _yaml_add_representer +from yaml import dump_all as _yaml_dump_all from yaml import safe_load_all as _yaml_safe_load_all from .common import unpack as _unpack @@ -618,6 +620,51 @@ _file_path = _path.join(_XDG_CONFIG_HOME, 'solaar', 'rules.yaml') rules = built_in_rules +def _save_config_rule_file(file_name=_file_path): + # This is a trick to show str/float/int lists in-line (inspired by https://stackoverflow.com/a/14001707) + class inline_list(list): + pass + + def blockseq_rep(dumper, data): + return dumper.represent_sequence(u'tag:yaml.org,2002:seq', data, flow_style=True) + + _yaml_add_representer(inline_list, blockseq_rep) + + def convert(elem): + if isinstance(elem, list): + if len(elem) == 1 and isinstance(elem[0], (int, str, float)): + # All diversion classes that expect a list of scalars also support a single scalar without a list + return elem[0] + if all(isinstance(c, (int, str, float)) for c in elem): + return inline_list([convert(c) for c in elem]) + return [convert(c) for c in elem] + if isinstance(elem, dict): + return {k: convert(v) for k, v in elem.items()} + return elem + + # YAML format settings + dump_settings = { + 'encoding': 'utf-8', + 'explicit_start': True, + 'explicit_end': True, + 'default_flow_style': False + # 'version': (1, 3), # it would be printed for every rule + } + # Save only user-defined rules + rules_to_save = sum([r.data()['Rule'] for r in rules.components if r.source == file_name], []) + if rules_to_save: + if _log.isEnabledFor(_INFO): + _log.info('saving %d rule(s) to %s', len(rules_to_save), file_name) + try: + with open(file_name, 'w') as f: + f.write('%YAML 1.3\n') # Write version manually + _yaml_dump_all(convert([r['Rule'] for r in rules_to_save]), f, **dump_settings) + except Exception as e: + _log.error('failed to save to %s\n%s', file_name, e) + return False + return True + + def _load_config_rule_file(): global rules loaded_rules = [] @@ -634,8 +681,7 @@ def _load_config_rule_file(): _log.info('loaded %d rules from %s', len(loaded_rules), config_file.name) except Exception as e: _log.error('failed to load from %s\n%s', _file_path, e) - loaded_rules.append(built_in_rules) - rules = Rule(loaded_rules) + rules = Rule([Rule(loaded_rules, source=_file_path), built_in_rules]) _load_config_rule_file() diff --git a/lib/solaar/ui/diversion_rules.py b/lib/solaar/ui/diversion_rules.py index ad4c014f..054ad2d1 100644 --- a/lib/solaar/ui/diversion_rules.py +++ b/lib/solaar/ui/diversion_rules.py @@ -21,7 +21,6 @@ from __future__ import absolute_import, division, print_function, unicode_litera from collections import defaultdict from contextlib import contextmanager as contextlib_contextmanager -from logging import INFO as _INFO from logging import getLogger from shlex import quote as shlex_quote @@ -32,7 +31,6 @@ from logitech_receiver import diversion as _DIV from logitech_receiver.special_keys import CONTROL as _CONTROL from pynput import mouse as _mouse from solaar.i18n import _ -from yaml import dump as _yaml_dump _log = getLogger(__name__) del getLogger @@ -154,7 +152,7 @@ class DiversionDialog: if response == Gtk.ResponseType.NO: w.hide() elif response == Gtk.ResponseType.YES: - self._save_yaml_file(_DIV._file_path) + self._save_yaml_file() w.hide() else: # don't close @@ -172,21 +170,11 @@ class DiversionDialog: self.view.set_model(self.model) self.view.expand_all() - def _save_yaml_file(self, file_name): - rules = [r.data()['Rule'] for r in _DIV.rules.components if r.source == file_name] - if len(rules) == 1: - rules = rules[0] - if rules: - if _log.isEnabledFor(_INFO): - _log.info('saving %d rule(s) to %s', len(rules), file_name) - try: - with open(file_name, 'w') as f: - _yaml_dump(rules, f, default_flow_style=False) - self.dirty = False - self.save_btn.set_sensitive(False) - self.discard_btn.set_sensitive(False) - except Exception as e: - _log.error('failed to save to %s\n%s', file_name, e) + def _save_yaml_file(self): + if _DIV._save_config_rule_file(): + self.dirty = False + self.save_btn.set_sensitive(False) + self.discard_btn.set_sensitive(False) def _create_top_panel(self): sw = Gtk.ScrolledWindow() @@ -205,7 +193,7 @@ class DiversionDialog: vbox = Gtk.VBox(spacing=20) self.save_btn = Gtk.Button(_('Save changes'), halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER, sensitive=False) self.discard_btn = Gtk.Button(_('Discard changes'), halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER, sensitive=False) - self.save_btn.connect('clicked', lambda *_args: self._save_yaml_file(_DIV._file_path)) + self.save_btn.connect('clicked', lambda *_args: self._save_yaml_file()) self.discard_btn.connect('clicked', lambda *_args: self._reload_yaml_file()) vbox.pack_start(self.save_btn, False, False, 0) vbox.pack_start(self.discard_btn, False, False, 0)