Repository: pwr-Solaar/Solaar

Issue:
Make UI react to device configuration changes done via CLI
**Information**
- Solaar version : 1.0.7
- Distribution: Mint & Pop!_OS
- Kernel version : 5.4.0 & 5.13.0
- Output of `solaar show` for the target device (if applicable): N/A

**Is your feature request related to a problem? Please describe.**
Configuration changes done via CLI are not visible on the UI until restart.

**Describe the solution you'd like**
I think the simplest way would be to dispatch D-Bus event when changing some setting with CLI with device and setting changed information. UI could then just query device for that setting state.


Edit:  The solution in PR #1499 is to use the facilities of GTK application to send an event from solaar config to the Solaar UI.


Discussion:
As we have mostly pointer and keyboard devices AND we want to update Solaar UI so some sort WM is running and we have user session and D-Bus should be there.
Corner cases when settings are changed, for example, by another user (root from cron maybe?) and not within WM could be ignored until someone really needs this...
There already is a case where some other program changes the settings - some other programs change the scroll wheel settings for mice.  There are also other programs that can change the settings of Logitech devices. The D-Bus solution is likely easier but would not catch these changes.
If we handle our own events via D-Bus and have code updating config state
for devices.
The Next step could be handling the events coming from the devices
themselves if possible. If the protocol or devices do not provide
consistent information we can't fix them, but having UI working and
updating itself on external events will allow to experiment with native
settings changes handling..
That sounds reasonable.   There is already a start at a D-Bus based low level control of devices.  See issue #654 for a pointer to it.
This is likely going to have to wait for a big cleanup of the low-level code in Solaar, either moving to a D-Bus solution or to a rewrite of how Solaar reads data from devices.  One problem with the current way Solaar reads information from a device is that there is a thread continuously listening to the device output (to process spontaneous output) and a different thread that can write to the device and then read from the devices, looking for the device to respond, indicating that the device successfully processed the write.  It is possible to tell the difference between spontaneous output and responses, so the continuously-listening thread can ignore responses.  But for Solaar to handle changes made by other programs, the continuously-listening thread needs to look for responses, and it is hard to determine whether the response is for a write from the other Solaar thread.  Treating all responses the same is costly because the response does not always contain current device state information.
One other problem here is that modern Linux drivers send commands to devices that result in responses that can confuse other software.   This probably needs to be fixed before a complete solution to this issue can be implemented.

Resolving patch:
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
@@ -16,11 +16,17 @@
 ## with this program; if not, write to the Free Software Foundation, Inc.,
 ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
+import yaml as _yaml
+
 from logitech_receiver import settings as _settings
 from logitech_receiver import settings_templates as _settings_templates
 from logitech_receiver.common import NamedInts as _NamedInts
 from solaar import configuration as _configuration
 
+import gi  # isort:skip
+gi.require_version('Gtk', '3.0')  # NOQA: E402
+from gi.repository import Gio, Gtk  # NOQA: E402 # isort:skip
+
 
 def _print_setting(s, verbose=True):
     print('#', s.label)
@@ -175,28 +181,42 @@ def run(receivers, args, find_receiver, find_device):
         _print_setting(setting)
         return
 
-    result, message, value = set(dev, setting, args)
+    APP_ID = 'io.github.pwr.solaar'
+    application = Gtk.Application.new(APP_ID, Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
+    application.register()
+
+    # if the Solaar UI is running don't save configuration here
+    result, message, value = set(dev, setting, args, not application.get_is_remote())
     if message is not None:
         print(message)
         if result is None:
             raise Exception("%s: failed to set value '%s' [%r]" % (setting.name, str(value), value))
 
+    # if the Solaar UI is running tell it to also perform the set
+    if application.get_is_remote():
+        argl = ['config', dev.serial, setting.name]
+        argl.extend([a for a in [args.value_key, args.extra_subkey, args.extra2] if a is not None])
+        application.run(_yaml.safe_dump(argl))
+
 
-def set(dev, setting, args):
+def set(dev, setting, args, save):
     if setting.kind == _settings.KIND.toggle:
         value = select_toggle(args.value_key, setting)
+        args.value_key = value
         message = 'Setting %s of %s to %s' % (setting.name, dev.name, value)
-        result = setting.write(value)
+        result = setting.write(value, save=save)
 
     elif setting.kind == _settings.KIND.range:
         value = select_range(args.value_key, setting)
+        args.value_key = value
         message = 'Setting %s of %s to %s' % (setting.name, dev.name, value)
-        result = setting.write(value)
+        result = setting.write(value, save=save)
 
     elif setting.kind == _settings.KIND.choice:
         value = select_choice(args.value_key, setting.choices, setting, None)
+        args.value_key = int(value)
         message = 'Setting %s of %s to %s' % (setting.name, dev.name, value)
-        result = setting.write(value)
+        result = setting.write(value, save=save)
 
     elif setting.kind == _settings.KIND.map_choice:
         if args.extra_subkey is None:
@@ -209,10 +229,12 @@ def set(dev, setting, args):
             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)
+            args.extra_subkey = int(value)
+            args.value_key = str(int(k))
         else:
             raise Exception("%s: key '%s' not in setting" % (setting.name, key))
         message = 'Setting %s of %s key %r to %r' % (setting.name, dev.name, k, value)
-        result = setting.write_key_value(int(k), value)
+        result = setting.write_key_value(int(k), value, save=save)
 
     elif setting.kind == _settings.KIND.multiple_toggle:
         if args.extra_subkey is None:
@@ -226,10 +248,12 @@ def set(dev, setting, args):
             k = next((k for k in setting._labels if ikey == k), None)
         if k is not None:
             value = select_toggle(args.extra_subkey, setting, key=k)
+            args.extra_subkey = value
+            args.value_key = str(int(k))
         else:
             raise Exception("%s: key '%s' not in setting" % (setting.name, key))
         message = 'Setting %s key %r to %r' % (setting.name, k, value)
-        result = setting.write_key_value(str(int(k)), value)
+        result = setting.write_key_value(str(int(k)), value, save=save)
 
     elif setting.kind == _settings.KIND.multiple_range:
         if args.extra_subkey is None:
@@ -247,10 +271,11 @@ def set(dev, setting, args):
         item = setting._value[k]
         if args.extra_subkey in item.keys():
             item[args.extra_subkey] = to_int(args.extra2)
+            args.value_key = str(int(k))
         else:
             raise Exception("%s: key '%s' not in setting" % (setting.name, key))
         message = 'Setting %s key %s parameter %s to %r' % (setting.name, k, args.extra_subkey, item[args.extra_subkey])
-        result = setting.write_key_value(int(k), item)
+        result = setting.write_key_value(int(k), item, save=save)
         value = item
 
     else:
diff --git a/lib/solaar/ui/__init__.py b/lib/solaar/ui/__init__.py
--- a/lib/solaar/ui/__init__.py
+++ b/lib/solaar/ui/__init__.py
@@ -17,12 +17,14 @@
 ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
 from logging import DEBUG as _DEBUG
+from logging import INFO as _INFO
 from logging import getLogger
 
+import yaml as _yaml
+
 import gi  # isort:skip
 gi.require_version('Gtk', '3.0')  # NOQA: E402
-
-from gi.repository import GLib, Gtk  # NOQA: E402 # isort:skip
+from gi.repository import GLib, Gtk, Gio  # NOQA: E402 # isort:skip
 from logitech_receiver.status import ALERT  # NOQA: E402 # isort:skip
 from solaar.i18n import _  # NOQA: E402 # isort:skip
 
@@ -126,9 +128,20 @@ def _activate(app):
 
 
 def _command_line(app, command_line):
-    if _log.isEnabledFor(_DEBUG):
-        _log.debug('command_line %s', command_line.get_arguments())
-
+    args = command_line.get_arguments()
+    args = _yaml.safe_load(''.join(args)) if args else args
+    if not args:
+        _activate(app)
+    elif args[0] == 'config':  # config call from remote instance
+        if _log.isEnabledFor(_INFO):
+            _log.info('remote command line %s', args)
+        from solaar.ui.config_panel import change_setting  # prevent circular import
+        from solaar.ui.window import find_device  # prevent circular import
+        dev = find_device(args[1])
+        if dev:
+            setting = next((s for s in dev.settings if s.name == args[2]), None)
+            if setting:
+                change_setting(dev, setting, args[3:])
     return 0
 
 
@@ -147,18 +160,18 @@ def _shutdown(app, shutdown_hook):
     notify.uninit()
 
 
-def run_loop(startup_hook, shutdown_hook, use_tray, show_window, args=None):
+def run_loop(startup_hook, shutdown_hook, use_tray, show_window):
     assert use_tray or show_window, 'need either tray or visible window'
     # from gi.repository.Gio import ApplicationFlags as _ApplicationFlags
     APP_ID = 'io.github.pwr.solaar'
-    application = Gtk.Application.new(APP_ID, 0)  # _ApplicationFlags.HANDLES_COMMAND_LINE)
+    application = Gtk.Application.new(APP_ID, Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
 
     application.connect('startup', lambda app, startup_hook: _startup(app, startup_hook, use_tray, show_window), startup_hook)
     application.connect('command-line', _command_line)
     application.connect('activate', _activate)
     application.connect('shutdown', _shutdown, shutdown_hook)
 
-    application.run(args)
+    application.run()
 
 
 #


Test patch: