Repository: pwr-Solaar/Solaar

Issue:
Command line to activate specific option
It is more a question than a feature request (but I don't know where to ask).

I know how to set the value of parameter "fn-lock" with a command: `~/Solaar/bin/solaar config 1 fn-swap 0`
 
However, I can't find how to fix "Natural scrolling" or "Zoom with two fingers" using a command. I found that "Natural scrolling corresponds to the parameter of id 45 in parameter gesture2-gestures but I did not find how to set it. 

Less important questions:
- how does "Tap and drag" works? After several tests I was not able to drag anything (I am using the last version of Solaar with a keyboard logitech K400+ on Ubuntu 20.04.1) ;
- what does "Swipe from the left edge" do? It does not seem to change anything when I activate it.

Thank you for this great tool!


Discussion:
The Solaar config command only works for simple settings.  Extending it should not be too hard, but it's not being worked on.
The gestures probably send some HID (or maybe HID++) message.  If your driver/OS/DM doesn't do anything with the message then there will not be any visible effect from turning the gesture on.
Ok thank you for the quick answer!
> how does "Tap and drag" works? After several tests I was not able to drag anything (I am using the last version of Solaar with a keyboard logitech K400+ on Ubuntu 20.04.1) ;

When the cursor is hovering over something that can be dragged, tap twice on the touchpad, but keep the finger pressed after the second tap; then, moving the cursor should drag the item. 


> what does "Swipe from the left edge" do? It does not seem to change anything when I activate it.

I'm not sure if this works the same way across different models, but my K400+ sends the following useless combinations (`F23` can be interpreted as `XF86TouchPadOff`).

```
LeftEdgeSwipe1Finger2   =>  Ctrl + Super + Backspace
RightEdgeSwipe1Finger2  =>   Alt + Super + F23
TopEdgeSwipe1Finger2    =>  Ctrl + Super + F23
```



Resolving patch:
diff --git a/lib/logitech_receiver/settings.py b/lib/logitech_receiver/settings.py
--- a/lib/logitech_receiver/settings.py
+++ b/lib/logitech_receiver/settings.py
@@ -271,6 +271,8 @@ def write_key_value(self, key, value):
             _log.debug('%s: settings write key %r value %r to %s', self.name, key, value, self._device)
 
         if self._device.online:
+            if not self._value:
+                self.read()
             try:
                 data_bytes = self._validator.prepare_write(int(key), value)
                 # always need to write to configuration because dictionary is shared and could have changed
@@ -373,6 +375,8 @@ def write_item_value(self, item, value):
             _log.debug('%s: settings write item %r value %r to %s', self.name, item, value, self._device)
 
         if self._device.online:
+            if not self._value:
+                self.read()
             data_bytes = self._validator.prepare_write_item(item, value)
             self._value[str(int(item))] = value
             self._pre_write()
@@ -471,6 +475,8 @@ def write_key_value(self, key, value):
             _log.debug('%s: settings write key %r value %r to %s', self.name, key, value, self._device)
 
         if self._device.online:
+            if not self._value:
+                self.read()
             value = bool(value)
             self._value[str(key)] = value
             self._pre_write()
diff --git a/lib/solaar/cli/__init__.py b/lib/solaar/cli/__init__.py
--- a/lib/solaar/cli/__init__.py
+++ b/lib/solaar/cli/__init__.py
@@ -68,7 +68,9 @@ def _create_parser():
         'or at least 3 characters of a device\'s name'
     )
     sp.add_argument('setting', nargs='?', help='device-specific setting; leave empty to list available settings')
-    sp.add_argument('value', nargs='?', help='new value for the setting')
+    sp.add_argument('value_key', nargs='?', help='new value for the setting or key for keyed settings')
+    sp.add_argument('extra_subkey', nargs='?', help='value for keyed or subkey for subkeyed settings')
+    sp.add_argument('extra2', nargs='?', help='value for subkeyed settings')
     sp.set_defaults(action='config')
 
     sp = subparsers.add_parser(
diff --git a/lib/solaar/cli/config.py b/lib/solaar/cli/config.py
--- a/lib/solaar/cli/config.py
+++ b/lib/solaar/cli/config.py
@@ -43,7 +43,68 @@ def _print_setting(s, verbose=True):
     if value is None:
         print(s.name, '= ? (failed to read from device)')
     else:
-        print(s.name, '= %r' % value)
+        print(s.name, '= %s' % value)
+
+
+def to_int(s):
+    try:
+        return int(s)
+    except ValueError:
+        return None
+
+
+def select_choice(value, choices, setting, key):
+    lvalue = value.lower()
+    ivalue = to_int(value)
+    val = None
+    for choice in choices:
+        if value == choice or ivalue is not None and ivalue == choice:
+            val = choice
+    if val is not None:
+        value = val
+    elif ivalue is not None and ivalue >= 0 and ivalue < len(choices):
+        value = choices[ivalue]
+    elif lvalue in ('higher', 'lower'):
+        old_value = setting.read() if key is None else setting.read_key(key)
+        if old_value is None:
+            raise Exception("%s: could not read current value'" % setting.name)
+        if lvalue == 'lower':
+            lower_values = choices[:old_value]
+            value = lower_values[-1] if lower_values else choices[:][0]
+        elif lvalue == 'higher':
+            higher_values = choices[old_value + 1:]
+            value = higher_values[0] if higher_values else choices[:][-1]
+    elif lvalue in ('highest', 'max', 'first'):
+        value = choices[:][-1]
+    elif lvalue in ('lowest', 'min', 'last'):
+        value = choices[:][0]
+    else:
+        raise Exception('%s: possible values are [%s]' % (setting.name, ', '.join(str(v) for v in choices)))
+    return value
+
+
+def select_toggle(value, setting):
+    try:
+        value = bool(int(value))
+    except Exception:
+        if value.lower() in ('true', 'yes', 'on', 't', 'y'):
+            value = True
+        elif value.lower() in ('false', 'no', 'off', 'f', 'n'):
+            value = False
+        else:
+            raise Exception("%s: don't know how to interpret '%s' as boolean" % (setting.name, value))
+    return value
+
+
+def select_range(value, setting):
+    try:
+        value = int(value)
+    except ValueError:
+        raise Exception("%s: can't interpret '%s' as integer" % (setting.name, value))
+    min, max = setting.range
+    if value < min or value > max:
+        raise Exception("%s: value '%s' out of bounds" % (setting.name, value))
+    return value
 
 
 def run(receivers, args, find_receiver, find_device):
@@ -79,66 +140,80 @@ def run(receivers, args, find_receiver, find_device):
         raise Exception("no setting '%s' for %s" % (args.setting, dev.name))
     _configuration.attach_to(dev)
 
-    if args.value is None:
+    if args.value_key is None:
         setting.apply()
         _print_setting(setting)
         return
 
     if setting.kind == _settings.KIND.toggle:
-        value = args.value
-        try:
-            value = bool(int(value))
-        except Exception:
-            if value.lower() in ('true', 'yes', 'on', 't', 'y'):
-                value = True
-            elif value.lower() in ('false', 'no', 'off', 'f', 'n'):
-                value = False
-            else:
-                raise Exception("%s: don't know how to interpret '%s' as boolean" % (setting.name, value))
+        value = select_toggle(args.value_key, setting)
         print('Setting %s of %s to %s' % (setting.name, dev.name, value))
+        result = setting.write(value)
 
     elif setting.kind == _settings.KIND.range:
-        try:
-            value = int(args.value)
-        except ValueError:
-            raise Exception("%s: can't interpret '%s' as integer" % (setting.name, args.value))
-        min, max = setting.range
-        if value < min or value > max:
-            raise Exception("%s: value '%s' out of bounds" % (setting.name, args.value))
+        value = select_range(args.value_key, setting)
         print('Setting %s of %s to %s' % (setting.name, dev.name, value))
+        result = setting.write(value)
 
     elif setting.kind == _settings.KIND.choice:
-        value = args.value
-        lvalue = value.lower()
-        try:
-            ivalue = int(value)
-        except ValueError:
-            ivalue = None
-        if value in setting.choices:
-            value = setting.choices[value]
-        elif ivalue is not None and ivalue >= 0 and ivalue < len(setting.choices):
-            value = setting.choices[ivalue]
-        elif lvalue in ('higher', 'lower'):
-            old_value = setting.read()
-            if old_value is None:
-                raise Exception("%s: could not read current value'" % setting.name)
-            if lvalue == 'lower':
-                lower_values = setting.choices[:old_value]
-                value = lower_values[-1] if lower_values else setting.choices[:][0]
-            elif lvalue == 'higher':
-                higher_values = setting.choices[old_value + 1:]
-                value = higher_values[0] if higher_values else setting.choices[:][-1]
-        elif lvalue in ('highest', 'max', 'first'):
-            value = setting.choices[:][-1]
-        elif lvalue in ('lowest', 'min', 'last'):
-            value = setting.choices[:][0]
-        else:
-            raise Exception('%s: possible values are [%s]' % (setting.name, ', '.join(str(v) for v in setting.choices)))
+        value = select_choice(args.value_key, setting.choices, setting, None)
         print('Setting %s of %s to %s' % (setting.name, dev.name, value))
+        result = setting.write(value)
+
+    elif setting.kind == _settings.KIND.map_choice:
+        key = args.value_key
+        ikey = to_int(key)
+        k = next((k for k in setting.choices.keys() if key == k), None)
+        if k is None and ikey is not None:
+            k = next((k for k in setting.choices.keys() if ikey == k), None)
+        if k is not None:
+            value = select_choice(args.extra_subkey, setting.choices[k], setting, key)
+        else:
+            raise Exception("%s: key '%s' not in setting" % (setting.name, key))
+        print('Setting %s of %s key %r to %r' % (setting.name, dev.name, k, value))
+        result = setting.write_key_value(int(k), value)
+
+    elif setting.kind == _settings.KIND.multiple_toggle:
+        key = args.value_key
+        ikey = to_int(key)
+        k = next((k for k in setting._labels if key == k), None)
+        if k is None and ikey is not None:
+            k = next((k for k in setting._labels if ikey == k), None)
+        if k is not None:
+            value = select_toggle(args.extra_subkey, setting)
+        else:
+            raise Exception("%s: key '%s' not in setting" % (setting.name, key))
+        print('Setting %s key %r to %r' % (setting.name, k, value))
+        result = setting.write_key_value(int(k), value)
+
+    elif setting.kind == _settings.KIND.multiple_range:
+        key = args.value_key
+        ikey = to_int(key)
+        if args.extra_subkey is None:
+            raise Exception('%s: setting needs a subkey' % (setting.name))
+        if args.extra2 is None or to_int(args.extra2) is None:
+            raise Exception('%s: setting needs an integer value, not %s' % (setting.name, args.extra2))
+        if not setting._value:  # ensure that there are values to look through
+            setting.read()
+        k = next((k for k in setting._value if key == k), None)
+        if k is None and ikey is not None:
+            k = next((k for k in setting._value if ikey == k), None)
+        item = setting._value[k]
+        if args.extra_subkey in item.keys():
+            item[args.extra_subkey] = to_int(args.extra2)
+        else:
+            raise Exception("%s: key '%s' not in setting" % (setting.name, key))
+        print('Setting %s key %s parameter %s to %r' % (setting.name, k, args.extra_subkey, item[args.extra_subkey]))
+        result = setting.write_item_value(int(k), item)
+
+
+# KIND = _NamedInts(, multiple_toggle=0x10, multiple_range=0x40)
+# BitField; MultipleRange
+# disable_keyboard_keys, gesture2; gesture2_params
+# k400+ / craft, k400+; k400+
 
     else:
         raise Exception('NotImplemented')
 
-    result = setting.write(value)
     if result is None:
         raise Exception("%s: failed to set value '%s' [%r]" % (setting.name, str(value), value))


Test patch: