Repository: pwr-Solaar/Solaar

Issue:
User rule cannot be deleted if it is the only one
**Information**
<!-- Make sure that your issue is not one of the known issues in the Solaar documentation at https://pwr-solaar.github.io/Solaar/ -->
<!-- Do not bother opening an issue for a version older than 1.1.0.  Upgrade to the latest version and see if your issue persists. -->
<!-- If you not running the current version of Solaar, strongly consider upgrading to the newest version. -->
- Solaar version (`solaar --version` or `git describe --tags` if cloned from this repository): 1.1.4
- Distribution: Linux Mint 20.3 Cinnamon
- Kernel version (ex. `uname -srmo`): `5.4.0-124-generic`
- Output of `solaar show`:

<details>

```
Solaar version 1.1.4

Unifying Receiver
  Device path  : /dev/hidraw0
  USB id       : 046d:C52B
  Serial       : 0F88276B
    Firmware   : 24.11.B0036
    Bootloader : 02.09
    Other      : AA.AC
  Has 1 paired device(s) out of a maximum of 6.
  Notifications: wireless, software present (0x000900)
  Device activity counters: 1=82

  1: MX Keys Keyboard
     Device path  : /dev/hidraw1
     WPID         : 408A
     Codename     : MX Keys
     Kind         : keyboard
     Protocol     : HID++ 4.5
     Polling rate : 20 ms (50Hz)
     Serial number: 3EB38AFE
     Model ID:      B35B408A0000
     Unit ID:       3EB38AFE
        Bootloader: BL1 08.00.B0011
          Firmware: MPK 12.01.B0013
             Other: 
     The power switch is located on the edge of top right corner.
     Supports 32 HID++ 2.0 features:
         0: ROOT                   {0000}   
         1: FEATURE SET            {0001}   
         2: DEVICE FW VERSION      {0003}   
            Firmware: Bootloader BL1 08.00.B0011 00008169E8BB
            Firmware: Firmware MPK 12.01.B0013 408AFE037737
            Firmware: Other   
            Unit ID: 3EB38AFE  Model ID: B35B408A0000  Transport IDs: {'btleid': 'B35B', 'wpid': '408A'}
         3: DEVICE NAME            {0005}   
            Name: MX Keys Wireless Keyboard
            Kind: keyboard
         4: WIRELESS DEVICE STATUS {1D4B}   
         5: RESET                  {0020}   
         6: DEVICE FRIENDLY NAME   {0007}   
            Friendly Name: MX Keys
         7: BATTERY STATUS         {1000}   
            Battery: 50%, discharging, next level 20%.
         8: REPROG CONTROLS V4     {1B04}   
            Key/Button Diversion (saved): {Calculator:Regular, Show Desktop:Regular, Lock PC:Regular, Screen Capture/Print Screen:Regular, Brightness Down:Regular, Brightness Up:Regular, Host Switch Channel 1:Regular, Host Switch Channel 2:Regular, Host Switch Channel 3:Regular, Mission Control/Task View:Regular, Dashboard Launchpad/Action Center:Regular, Backlight Down:Regular, Backlight Up:Regular, Previous Fn:Regular, Play/Pause Fn:Regular, Next Fn:Regular, Mute Fn:Regular, Volume Down Fn:Regular, Volume Up Fn:Regular, App Contextual Menu/Right Click:Regular, Right Arrow:Regular, Left Arrow:Regular}
            Key/Button Diversion        : {Calculator:Regular, Show Desktop:Regular, Lock PC:Regular, Screen Capture/Print Screen:Regular, Brightness Down:Regular, Brightness Up:Regular, Host Switch Channel 1:Regular, Host Switch Channel 2:Regular, Host Switch Channel 3:Regular, Mission Control/Task View:Regular, Dashboard Launchpad/Action Center:Regular, Backlight Down:Regular, Backlight Up:Regular, Previous Fn:Regular, Play/Pause Fn:Regular, Next Fn:Regular, Mute Fn:Regular, Volume Down Fn:Regular, Volume Up Fn:Regular, App Contextual Menu/Right Click:Regular, Right Arrow:Regular, Left Arrow:Regular}
         9: CHANGE HOST            {1814}   
            Change Host        : 1:jv-desktop
        10: HOSTS INFO             {1815}   
            Host 0 (paired): jv-desktop
            Host 1 (unpaired): 
            Host 2 (unpaired): 
        11: BACKLIGHT2             {1982}   
            Backlight (saved): True
            Backlight        : True
        12: K375S FN INVERSION     {40A3}   
            Swap Fx function (saved): False
            Swap Fx function        : False
        13: ENCRYPTION             {4100}   
        14: LOCK KEY STATE         {4220}   
        15: KEYBOARD DISABLE KEYS  {4521}   
            Disable keys (saved): {Caps Lock:False, Num Lock:False, Scroll Lock:False, Insert:False, Win:False}
            Disable keys        : {Caps Lock:False, Num Lock:False, Scroll Lock:False, Insert:False, Win:False}
        16: MULTIPLATFORM          {4531}   
            Set OS (saved): Windows
            Set OS        : Windows
        17: DFUCONTROL SIGNED      {00C2}   
        18: DEVICE RESET           {1802}   internal, hidden
        19: unknown:1803           {1803}   internal, hidden
        20: CONFIG DEVICE PROPS    {1806}   internal, hidden
        21: unknown:1813           {1813}   internal, hidden
        22: OOBSTATE               {1805}   internal, hidden
        23: unknown:1830           {1830}   internal, hidden
        24: unknown:1890           {1890}   internal, hidden
        25: unknown:1891           {1891}   internal, hidden
        26: unknown:18A1           {18A1}   internal, hidden
        27: unknown:1DF3           {1DF3}   internal, hidden
        28: unknown:1E00           {1E00}   hidden
        29: unknown:1EB0           {1EB0}   internal, hidden
        30: unknown:1861           {1861}   internal, hidden
        31: unknown:1A20           {1A20}   internal, hidden
     Has 24 reprogrammable keys:
         0: Host Switch Channel 1     , default: HostSwitch Channel 1        => HostSwitch Channel 1      
             nonstandard, divertable, persistently divertable, analytics key events, pos:0, group:0, group mask:empty
             reporting: default
         1: Host Switch Channel 2     , default: HostSwitch Channel 2        => HostSwitch Channel 2      
             nonstandard, divertable, persistently divertable, analytics key events, pos:0, group:0, group mask:empty
             reporting: default
         2: Host Switch Channel 3     , default: HostSwitch Channel 3        => HostSwitch Channel 3      
             nonstandard, divertable, persistently divertable, analytics key events, pos:0, group:0, group mask:empty
             reporting: default
         3: Brightness Down           , default: Brightness Down             => Brightness Down           
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, analytics key events, pos:1, group:0, group mask:empty
             reporting: default
         4: Brightness Up             , default: Brightness Up               => Brightness Up             
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, analytics key events, pos:2, group:0, group mask:empty
             reporting: default
         5: Mission Control/Task View , default: Mission Control/Task View   => Mission Control/Task View 
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, analytics key events, pos:3, group:0, group mask:empty
             reporting: default
         6: Dashboard Launchpad/Action Center, default: Dashboard Launchpad/Action Center => Dashboard Launchpad/Action Center
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, analytics key events, pos:4, group:0, group mask:empty
             reporting: default
         7: Show Desktop              , default: Show Desktop                => Show Desktop              
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, analytics key events, pos:5, group:0, group mask:empty
             reporting: default
         8: Backlight Down            , default: Backlight Down              => Backlight Down            
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, analytics key events, pos:6, group:0, group mask:empty
             reporting: default
         9: Backlight Up              , default: Backlight Up                => Backlight Up              
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, analytics key events, pos:7, group:0, group mask:empty
             reporting: default
        10: Previous Fn               , default: Previous                    => Previous                  
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, analytics key events, pos:8, group:0, group mask:empty
             reporting: default
        11: Play/Pause Fn             , default: Play/Pause                  => Play/Pause                
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, analytics key events, pos:9, group:0, group mask:empty
             reporting: default
        12: Next Fn                   , default: Next                        => Next                      
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, analytics key events, pos:10, group:0, group mask:empty
             reporting: default
        13: Mute Fn                   , default: Mute                        => Mute                      
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, analytics key events, pos:11, group:0, group mask:empty
             reporting: default
        14: Volume Down Fn            , default: Volume Down                 => Volume Down               
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, analytics key events, pos:12, group:0, group mask:empty
             reporting: default
        15: Volume Up Fn              , default: Volume Up                   => Volume Up                 
             nonstandard, reprogrammable, divertable, persistently divertable, analytics key events, pos:0, group:0, group mask:empty
             reporting: default
        16: Calculator                , default: Calculator                  => Calculator                
             nonstandard, reprogrammable, divertable, persistently divertable, analytics key events, pos:0, group:0, group mask:empty
             reporting: default
        17: Screen Capture/Print Screen, default: Screen Capture              => Screen Capture            
             nonstandard, reprogrammable, divertable, persistently divertable, analytics key events, pos:0, group:0, group mask:empty
             reporting: default
        18: App Contextual Menu/Right Click, default: Right Click/App Contextual Menu => Right Click/App Contextual Menu
             nonstandard, reprogrammable, divertable, persistently divertable, analytics key events, pos:0, group:0, group mask:empty
             reporting: default
        19: Lock PC                   , default: WindowsLock                 => WindowsLock               
             nonstandard, reprogrammable, divertable, persistently divertable, analytics key events, pos:0, group:0, group mask:empty
             reporting: default
        20: Left Arrow                , default: Keyboard Left Arrow         => Keyboard Left Arrow       
             nonstandard, divertable, persistently divertable, analytics key events, pos:0, group:0, group mask:empty
             reporting: default
        21: Right Arrow               , default: Keyboard Right Arrow        => Keyboard Right Arrow      
             nonstandard, divertable, persistently divertable, analytics key events, pos:0, group:0, group mask:empty
             reporting: default
        22: F Lock                    , default: Do Nothing One              => Do Nothing One            
             is FN, analytics key events, pos:0, group:0, group mask:empty
             reporting: default
        23: FN Key                    , default: Do Nothing One              => Do Nothing One            
             nonstandard, analytics key events, pos:0, group:0, group mask:empty
             reporting: default
     Battery: 50%, discharging, next level 20%.

```
</details>

- Contents of `~/.config/solaar/config.yaml` (or `~/.config/solaar/config.json` if `~/.config/solaar/config.yaml` not present):

<details>

```
- 1.1.4
- _NAME: MX Keys Keyboard
  _absent: [hi-res-scroll, lowres-scroll-mode, hires-smooth-invert, hires-smooth-resolution, hires-scroll-mode, smart-shift, thumb-scroll-invert, thumb-scroll-mode,
    onboard_profiles, report_rate, pointer_speed, dpi, dpi-sliding, speed-change, mouse-gestures, backlight-timed, reprogrammable-keys, persistent-remappable-keys,
    crown-smooth, divert-crown, divert-gkeys, m-key-leds, mr-key-led, gesture2-gestures, gesture2-divert, gesture2-params, sidetone, equalizer]
  _modelId: B35B408A0000
  _sensitive: {backlight: false, change-host: false, divert-keys: false, fn-swap: false, multiplatform: false}
  _serial: 3EB38AFE
  _unitId: 3EB38AFE
  _wpid: 408A
  backlight: true
  change-host: null
  disable-keyboard-keys: {1: false, 2: false, 4: false, 8: false, 16: false}
  divert-keys: {10: 0, 110: 0, 111: 0, 191: 0, 199: 0, 200: 0, 209: 0, 210: 0, 211: 0, 224: 0, 225: 0, 226: 0, 227: 0, 228: 0, 229: 0, 230: 0, 231: 0, 232: 0,
    233: 0, 234: 0, 235: 0, 236: 0}
  fn-swap: false
  multiplatform: 0
```
</details>


- Errors or warrnings from Solaar:
06:43:11,481  WARNING [MainThread] logitech_receiver.diversion: rule Key arguments unknown: 
06:43:11,482  WARNING [MainThread] logitech_receiver.diversion: rule Key key name not name of a Logitech key: 0
06:43:15,217  WARNING [MainThread] logitech_receiver.diversion: rule Key key name not name of a Logitech key: c
06:43:15,518  WARNING [MainThread] logitech_receiver.diversion: rule Key key name not name of a Logitech key: ca
06:43:15,720  WARNING [MainThread] logitech_receiver.diversion: rule Key key name not name of a Logitech key: cal
06:43:32,172  WARNING [MainThread] logitech_receiver.diversion: rule KeyPress keys not key names ['']

(Happened only once during reproducing the bug, not sure how relevant it is)


**Describe the bug**
Saving changes after deleting the last user rule has no effect. After restarting Solaar, the deleted rule is again active and present in the rule editor. Editing or deleting conditions or actions in the rule is saved correctly, but deleting the rule itself is not.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to 'Rule Editor'
2. Add a user defined rule
3. Add condition
4. Add action
5. Save changes
6. Restart Solaar
7. Go to 'Rule Editor'
8. The deleted rule is back

**Screenshots**
![SolaarBugScreenshot2](https://user-images.githubusercontent.com/52212021/185777159-d25c9d8a-cffc-4c1c-ac87-1f9d8c571dec.png)


**Additional context**

- Problem is not in the rule itself, it is possible to add new rule, delete the problematic one and this change will be saved correctly. But the new rule will have the same issue.
- Even an empty rule (without any conditions or actions) is kept.
- File rules.yaml is without changes after saving the deletion of the rule.
- Deleting file rules.yaml seems to solve the issue.



Discussion:

Resolving patch:
diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py
--- a/lib/logitech_receiver/diversion.py
+++ b/lib/logitech_receiver/diversion.py
@@ -380,7 +380,7 @@ def compile(self, c):
 
 
 class Rule(RuleComponent):
-    def __init__(self, args, source=None):
+    def __init__(self, args, source=None, warn=True):
         self.components = [self.compile(a) for a in args]
         self.source = source
 
@@ -414,7 +414,7 @@ def evaluate(self, feature, notification, device, status, last_result):
 
 
 class Not(Condition):
-    def __init__(self, op):
+    def __init__(self, op, warn=True):
         if isinstance(op, list) and len(op) == 1:
             op = op[0]
         self.op = op
@@ -432,7 +432,7 @@ def data(self):
 
 
 class Or(Condition):
-    def __init__(self, args):
+    def __init__(self, args, warn=True):
         self.components = [self.compile(a) for a in args]
 
     def __str__(self):
@@ -453,7 +453,7 @@ def data(self):
 
 
 class And(Condition):
-    def __init__(self, args):
+    def __init__(self, args, warn=True):
         self.components = [self.compile(a) for a in args]
 
     def __str__(self):
@@ -506,12 +506,14 @@ def x11_pointer_prog():
 
 
 class Process(Condition):
-    def __init__(self, process):
+    def __init__(self, process, warn=True):
         self.process = process
         if wayland or not x11_setup():
-            _log.warn('rules can only access active process in X11 - %s', self)
+            if warn:
+                _log.warn('rules can only access active process in X11 - %s', self)
         if not isinstance(process, str):
-            _log.warn('rule Process argument not a string: %s', process)
+            if warn:
+                _log.warn('rule Process argument not a string: %s', process)
             self.process = str(process)
 
     def __str__(self):
@@ -529,12 +531,14 @@ def data(self):
 
 
 class MouseProcess(Condition):
-    def __init__(self, process):
+    def __init__(self, process, warn=True):
         self.process = process
         if wayland or not x11_setup():
-            _log.warn('rules cannot access active mouse process in X11 - %s', self)
+            if warn:
+                _log.warn('rules cannot access active mouse process in X11 - %s', self)
         if not isinstance(process, str):
-            _log.warn('rule MouseProcess argument not a string: %s', process)
+            if warn:
+                _log.warn('rule MouseProcess argument not a string: %s', process)
             self.process = str(process)
 
     def __str__(self):
@@ -552,9 +556,10 @@ def data(self):
 
 
 class Feature(Condition):
-    def __init__(self, feature):
+    def __init__(self, feature, warn=True):
         if not (isinstance(feature, str) and feature in _F):
-            _log.warn('rule Feature argument not name of a feature: %s', feature)
+            if warn:
+                _log.warn('rule Feature argument not name of a feature: %s', feature)
             self.feature = None
         self.feature = _F[feature]
 
@@ -569,9 +574,10 @@ def data(self):
 
 
 class Report(Condition):
-    def __init__(self, report):
+    def __init__(self, report, warn=True):
         if not (isinstance(report, int)):
-            _log.warn('rule Report argument not an integer: %s', report)
+            if warn:
+                _log.warn('rule Report argument not an integer: %s', report)
             self.report = -1
         self.report = report
 
@@ -587,9 +593,10 @@ def data(self):
 
 # Setting(device, setting, [key], value...)
 class Setting(Condition):
-    def __init__(self, args):
+    def __init__(self, args, warn=True):
         if not (isinstance(args, list) and len(args) > 2):
-            _log.warn('rule Setting argument not list with minimum length 3: %s', args)
+            if warn:
+                _log.warn('rule Setting argument not list with minimum length 3: %s', args)
             self.args = []
         else:
             self.args = args
@@ -631,7 +638,7 @@ def data(self):
 
 
 class Modifiers(Condition):
-    def __init__(self, modifiers):
+    def __init__(self, modifiers, warn=True):
         modifiers = [modifiers] if isinstance(modifiers, str) else modifiers
         self.desired = 0
         self.modifiers = []
@@ -640,7 +647,8 @@ def __init__(self, modifiers):
                 self.desired += MODIFIERS.get(k, 0)
                 self.modifiers.append(k)
             else:
-                _log.warn('unknown rule Modifier value: %s', k)
+                if warn:
+                    _log.warn('unknown rule Modifier value: %s', k)
 
     def __str__(self):
         return 'Modifiers: ' + str(self.desired)
@@ -661,14 +669,15 @@ class Key(Condition):
     DOWN = 'pressed'
     UP = 'released'
 
-    def __init__(self, args):
+    def __init__(self, args, warn=True):
         default_key = 0
         default_action = self.DOWN
 
         key, action = None, None
 
         if not args or not isinstance(args, (list, str)):
-            _log.warn('rule Key arguments unknown: %s' % args)
+            if warn:
+                _log.warn('rule Key arguments unknown: %s' % args)
             key = default_key
             action = default_action
         elif isinstance(args, str):
@@ -685,13 +694,15 @@ def __init__(self, args):
         if isinstance(key, str) and key in _CONTROL:
             self.key = _CONTROL[key]
         else:
-            _log.warn('rule Key key name not name of a Logitech key: %s' % key)
+            if warn:
+                _log.warn('rule Key key name not name of a Logitech key: %s' % key)
             self.key = default_key
 
         if isinstance(action, str) and action in (self.DOWN, self.UP):
             self.action = action
         else:
-            _log.warn('rule Key action unknown: %s, assuming %s' % (action, default_action))
+            if warn:
+                _log.warn('rule Key action unknown: %s, assuming %s' % (action, default_action))
             self.action = default_action
 
     def __str__(self):
@@ -717,24 +728,28 @@ def range_test_helper(f, r, d):
 
 
 class Test(Condition):
-    def __init__(self, test):
+    def __init__(self, test, warn=True):
         self.test = test
         if isinstance(test, str):
             if test in MOUSE_GESTURE_TESTS:
-                _log.warn('mouse movement test %s deprecated, converting to a MouseGesture', test)
+                if warn:
+                    _log.warn('mouse movement test %s deprecated, converting to a MouseGesture', test)
                 self.__class__ = MouseGesture
-                self.__init__(MOUSE_GESTURE_TESTS[test])
+                self.__init__(MOUSE_GESTURE_TESTS[test], warn=warn)
             elif test in TESTS:
                 self.function = TESTS[test]
             else:
-                _log.warn('rule Test string argument not name of a test: %s', test)
+                if warn:
+                    _log.warn('rule Test string argument not name of a test: %s', test)
                 self.function = TESTS['False']
         elif isinstance(test, list) and all(isinstance(t, int) for t in test):
-            _log.warn('Test rules consisting of numbers are deprecated, converting to a TestBytes condition')
+            if warn:
+                _log.warn('Test rules consisting of numbers are deprecated, converting to a TestBytes condition')
             self.__class__ = TestBytes
-            self.__init__(test)
+            self.__init__(test, warn=warn)
         else:
-            _log.warn('rule Test argument not valid %s', test)
+            if warn:
+                _log.warn('rule Test argument not valid %s', test)
 
     def __str__(self):
         return 'Test: ' + str(self.test)
@@ -747,7 +762,7 @@ def data(self):
 
 
 class TestBytes(Condition):
-    def __init__(self, test):
+    def __init__(self, test, warn=True):
         self.test = test
         if (
             isinstance(test, list) and 2 < len(test) <= 4 and all(isinstance(t, int) for t in test) and test[0] >= 0
@@ -755,7 +770,8 @@ def __init__(self, test):
         ):
             self.function = bit_test(*test) if len(test) == 3 else range_test(*test)
         else:
-            _log.warn('rule TestBytes argument not valid %s', test)
+            if warn:
+                _log.warn('rule TestBytes argument not valid %s', test)
 
     def __str__(self):
         return 'TestBytes: ' + str(self.test)
@@ -773,12 +789,13 @@ class MouseGesture(Condition):
         'Mouse Down-right'
     ]
 
-    def __init__(self, movements):
+    def __init__(self, movements, warn=True):
         if isinstance(movements, str):
             movements = [movements]
         for x in movements:
             if x not in self.MOVEMENTS and x not in _CONTROL:
-                _log.warn('rule Mouse Gesture argument not direction or name of a Logitech key: %s', x)
+                if warn:
+                    _log.warn('rule Mouse Gesture argument not direction or name of a Logitech key: %s', x)
         self.movements = movements
 
     def __str__(self):
@@ -826,15 +843,17 @@ def evaluate(self, feature, notification, device, status, last_result):
 class KeyPress(Action):
     CLICK, DEPRESS, RELEASE = 'click', 'depress', 'release'
 
-    def __init__(self, args):
+    def __init__(self, args, warn=True):
         self.key_names, self.action = self.regularize_args(args)
         if not isinstance(self.key_names, list):
-            _log.warn('rule KeyPress keys not key names %s', self.keys_names)
+            if warn:
+                _log.warn('rule KeyPress keys not key names %s', self.keys_names)
             self.key_symbols = []
         else:
             self.key_symbols = [XK_KEYS.get(k, None) for k in self.key_names]
         if not all(self.key_symbols):
-            _log.warn('rule KeyPress keys not key names %s', self.key_names)
+            if warn:
+                _log.warn('rule KeyPress keys not key names %s', self.key_names)
             self.key_symbols = []
 
     def regularize_args(self, args):
@@ -927,12 +946,13 @@ def data(self):
 
 
 class MouseScroll(Action):
-    def __init__(self, amounts):
+    def __init__(self, amounts, warn=True):
         import numbers
         if len(amounts) == 1 and isinstance(amounts[0], list):
             amounts = amounts[0]
         if not (len(amounts) == 2 and all([isinstance(a, numbers.Number) for a in amounts])):
-            _log.warn('rule MouseScroll argument not two numbers %s', amounts)
+            if warn:
+                _log.warn('rule MouseScroll argument not two numbers %s', amounts)
             amounts = [0, 0]
         self.amounts = amounts
 
@@ -957,20 +977,22 @@ def data(self):
 
 
 class MouseClick(Action):
-    def __init__(self, args):
+    def __init__(self, args, warn=True):
         if len(args) == 1 and isinstance(args[0], list):
             args = args[0]
         if not isinstance(args, list):
             args = [args]
         self.button = str(args[0]) if len(args) >= 0 else None
         if self.button not in buttons:
-            _log.warn('rule MouseClick action: button %s not known', self.button)
+            if warn:
+                _log.warn('rule MouseClick action: button %s not known', self.button)
             self.button = None
         count = args[1] if len(args) >= 2 else 1
         try:
             self.count = int(count)
         except (ValueError, TypeError):
-            _log.warn('rule MouseClick action: count %s should be an integer', count)
+            if warn:
+                _log.warn('rule MouseClick action: count %s should be an integer', count)
             self.count = 1
 
     def __str__(self):
@@ -989,9 +1011,10 @@ def data(self):
 
 
 class Set(Action):
-    def __init__(self, args):
+    def __init__(self, args, warn=True):
         if not (isinstance(args, list) and len(args) > 2):
-            _log.warn('rule Set argument not list with minimum length 3: %s', args)
+            if warn:
+                _log.warn('rule Set argument not list with minimum length 3: %s', args)
             self.args = []
         else:
             self.args = args
@@ -1027,11 +1050,12 @@ def data(self):
 
 
 class Execute(Action):
-    def __init__(self, args):
+    def __init__(self, args, warn=True):
         if isinstance(args, str):
             args = [args]
         if not (isinstance(args, list) and all(isinstance(arg), str) for arg in args):
-            _log.warn('rule Execute argument not list of strings: %s', args)
+            if warn:
+                _log.warn('rule Execute argument not list of strings: %s', args)
             self.args = []
         else:
             self.args = args
@@ -1189,7 +1213,7 @@ def convert(elem):
     }
     # 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 True:  # save even if there are no rules to save
         if _log.isEnabledFor(_INFO):
             _log.info('saving %d rule(s) to %s', len(rules_to_save), file_name)
         try:
diff --git a/lib/solaar/ui/diversion_rules.py b/lib/solaar/ui/diversion_rules.py
--- a/lib/solaar/ui/diversion_rules.py
+++ b/lib/solaar/ui/diversion_rules.py
@@ -498,7 +498,7 @@ def _menu_do_insert(self, _mitem, m, it, new_c, below=False):
             self.view.expand_row(m.get_path(new_iter), True)
 
     def _menu_do_insert_new(self, _mitem, m, it, cls, initial_value, below=False):
-        new_c = cls(initial_value)
+        new_c = cls(initial_value, warn=False)
         return self._menu_do_insert(_mitem, m, it, new_c, below=below)
 
     def _menu_insert(self, m, it, below=False):
@@ -612,7 +612,7 @@ def _menu_do_wrap(self, _mitem, m, it, cls):
         parent_it = m.iter_parent(it)
         parent_c = m[parent_it][0].component
         if isinstance(parent_c, _DIV.Not):
-            new_c = cls([c])
+            new_c = cls([c], warn=False)
             parent_c.component = new_c
             m.remove(it)
             self._populate_model(m, parent_it, new_c, level=wrapped.level, pos=0)
@@ -1013,7 +1013,7 @@ def ignore_changes(self):
     def _on_update(self, *_args):
         if not self._ignore_changes and self.component is not None:
             value = self.collect_value()
-            self.component.__init__(value)
+            self.component.__init__(value, warn=False)
             self._on_update_callback()
             return value
         return None
@@ -1554,14 +1554,14 @@ def _create_del_btn(self):
         return btn
 
     def _clicked_add(self, _btn):
-        self.component.__init__(self.collect_value() + [''])
+        self.component.__init__(self.collect_value() + [''], warn=False)
         self.show(self.component, editable=True)
         self.fields[len(self.component.movements) - 1].grab_focus()
 
     def _clicked_del(self, _btn, pos):
         v = self.collect_value()
         v.pop(pos)
-        self.component.__init__(v)
+        self.component.__init__(v, warn=False)
         self.show(self.component, editable=True)
         self._on_update_callback()
 
@@ -1654,14 +1654,14 @@ def _create_del_btn(self):
 
     def _clicked_add(self, _btn):
         keys, action = self.component.regularize_args(self.collect_value())
-        self.component.__init__([keys + [''], action])
+        self.component.__init__([keys + [''], action], warn=False)
         self.show(self.component, editable=True)
         self.fields[len(self.component.key_names) - 1].grab_focus()
 
     def _clicked_del(self, _btn, pos):
         keys, action = self.component.regularize_args(self.collect_value())
         keys.pop(pos)
-        self.component.__init__([keys, action])
+        self.component.__init__([keys, action], warn=False)
         self.show(self.component, editable=True)
         self._on_update_callback()
 
@@ -1833,14 +1833,14 @@ def _create_del_btn(self):
         return btn
 
     def _clicked_add(self, *_args):
-        self.component.__init__(self.collect_value() + [''])
+        self.component.__init__(self.collect_value() + [''], warn=False)
         self.show(self.component, editable=True)
         self.fields[len(self.component.args) - 1].grab_focus()
 
     def _clicked_del(self, _btn, pos):
         v = self.collect_value()
         v.pop(pos)
-        self.component.__init__(v)
+        self.component.__init__(v, warn=False)
         self.show(self.component, editable=True)
         self._on_update_callback()
 


Test patch: