From c647d6df9998b2d5684d130143ea7d64ad8a125c Mon Sep 17 00:00:00 2001 From: Benjamin Tissoires Date: Wed, 2 Aug 2017 14:21:43 +0200 Subject: [PATCH] profiles: add name get/set capability Some devices can have a custom name, so let's export that capability. In ratbagd, libratbag/ratbag_profile_set_name() returns an error, we put it under silence, the client has to check for the capabilities before asking ratbagd to change the profile name. Fixes: #266 Signed-off-by: Benjamin Tissoires --- ratbagd/ratbagd-profile.c | 56 +++++++++++++++++++++++++++++++++++++++ ratbagd/ratbagd.c | 1 + src/libratbag-private.h | 1 + src/libratbag-util.h | 18 +++++++++++++ src/libratbag.c | 29 ++++++++++++++++++++ src/libratbag.h | 33 +++++++++++++++++++++++ src/libratbag.sym | 2 ++ tools/ratbagctl.in | 43 +++++++++++++++++++++++++++++- tools/ratbagctl.test.in | 28 ++++++++++++++++++++ tools/ratbagd.py | 16 ++++++++++- tools/toolbox.py | 4 ++- 11 files changed, 228 insertions(+), 3 deletions(-) diff --git a/ratbagd/ratbagd-profile.c b/ratbagd/ratbagd-profile.c index f640ad62..b861afa5 100644 --- a/ratbagd/ratbagd-profile.c +++ b/ratbagd/ratbagd-profile.c @@ -347,6 +347,57 @@ ratbagd_profile_is_enabled(sd_bus *bus, return sd_bus_message_append(reply, "b", enabled); } +static int +ratbagd_profile_set_name(sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *m, + void *userdata, + sd_bus_error *error) +{ + struct ratbagd_profile *profile = userdata; + char *name; + int r; + + r = sd_bus_message_read(m, "s", &name); + if (r < 0) + return r; + + r = ratbag_profile_set_name(profile->lib_profile, name); + + if (r == 0) { + sd_bus_emit_properties_changed(bus, + profile->path, + RATBAGD_NAME_ROOT ".Profile", + "Name", + NULL); + } + + /* + * Note: we should return `r`, but the d-bus bindings doesn't + * like non standard errors. So just return success and let the client + * check for the capability before-hand and let .commit() signal an + * error. + */ + return 0; +} + +static int +ratbagd_profile_get_name(sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) +{ + struct ratbagd_profile *profile = userdata; + const char *name = ratbag_profile_get_name(profile->lib_profile); + + return sd_bus_message_append(reply, "s", name); +} + static int ratbagd_profile_get_capabilities(sd_bus *bus, const char *path, @@ -360,6 +411,7 @@ ratbagd_profile_get_capabilities(sd_bus *bus, struct ratbag_profile *lib_profile = profile->lib_profile; enum ratbag_profile_capability cap; enum ratbag_profile_capability caps[] = { + RATBAG_PROFILE_CAP_WRITABLE_NAME, }; int r; size_t i; @@ -382,6 +434,10 @@ ratbagd_profile_get_capabilities(sd_bus *bus, const sd_bus_vtable ratbagd_profile_vtable[] = { SD_BUS_VTABLE_START(0), + SD_BUS_WRITABLE_PROPERTY("Name", "s", + ratbagd_profile_get_name, + ratbagd_profile_set_name, 0, + SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), SD_BUS_WRITABLE_PROPERTY("Enabled", "b", ratbagd_profile_is_enabled, ratbagd_profile_set_enabled, 0, diff --git a/ratbagd/ratbagd.c b/ratbagd/ratbagd.c index ab32ff42..f71eaaa6 100644 --- a/ratbagd/ratbagd.c +++ b/ratbagd/ratbagd.c @@ -266,6 +266,7 @@ static const struct ratbag_test_device ratbagd_test_device_descr = { }, .active = false, .dflt = true, + .caps = { RATBAG_PROFILE_CAP_WRITABLE_NAME }, }, { .buttons = { diff --git a/src/libratbag-private.h b/src/libratbag-private.h index 98764f0d..01a925a6 100644 --- a/src/libratbag-private.h +++ b/src/libratbag-private.h @@ -261,6 +261,7 @@ struct ratbag_led { struct ratbag_profile { int refcount; void *userdata; + char *name; struct list link; unsigned index; diff --git a/src/libratbag-util.h b/src/libratbag-util.h index 9de532aa..a8dcb037 100644 --- a/src/libratbag-util.h +++ b/src/libratbag-util.h @@ -118,6 +118,24 @@ snprintf_safe(char *buf, size_t n, const char *fmt, ...) #define sprintf_safe(buf, fmt, ...) \ snprintf_safe(buf, ARRAY_LENGTH(buf), fmt, __VA_ARGS__) +__attribute__((format(printf, 1, 2))) +static inline void * +asprintf_safe(const char *fmt, ...) +{ + va_list args; + int rc; + char *result; + + va_start(args, fmt); + rc = vasprintf(&result, fmt, args); + va_end(args); + + if (rc < 0) + abort(); + + return result; +} + static inline void msleep(unsigned int ms) { diff --git a/src/libratbag.c b/src/libratbag.c index 7e49e018..b8d46901 100644 --- a/src/libratbag.c +++ b/src/libratbag.c @@ -663,6 +663,7 @@ ratbag_create_profile(struct ratbag_device *device, sizeof(*profile->resolution.modes)); profile->resolution.num_modes = num_resolutions; profile->is_enabled = true; + profile->name = asprintf_safe("Profile %d", index + 1); list_insert(&device->profiles, &profile->link); list_init(&profile->buttons); @@ -743,6 +744,9 @@ ratbag_profile_destroy(struct ratbag_profile *profile) free(profile->resolution.modes); + if (profile->name) + free(profile->name); + list_remove(&profile->link); free(profile); } @@ -1532,6 +1536,31 @@ ratbag_profile_get_led(struct ratbag_profile *profile, return NULL; } +LIBRATBAG_EXPORT const char * +ratbag_profile_get_name(struct ratbag_profile *profile) +{ + return profile->name; +} + +LIBRATBAG_EXPORT int +ratbag_profile_set_name(struct ratbag_profile *profile, + const char *name) +{ + char *name_copy; + + if (!ratbag_profile_has_capability(profile, + RATBAG_PROFILE_CAP_WRITABLE_NAME)) + return RATBAG_ERROR_CAPABILITY; + + name_copy = strdup_safe(name); + if (profile->name) + free(profile->name); + + profile->name = name_copy; + + return 0; +} + LIBRATBAG_EXPORT void ratbag_set_user_data(struct ratbag *ratbag, void *userdata) { diff --git a/src/libratbag.h b/src/libratbag.h index a85d3697..60bb866b 100644 --- a/src/libratbag.h +++ b/src/libratbag.h @@ -651,6 +651,14 @@ ratbag_device_get_num_leds(struct ratbag_device *device); */ enum ratbag_profile_capability { RATBAG_PROFILE_CAP_NONE = 0, + /** + * The device has the capability to write a name to a profile + * and store it in its flash. + * + * Any changes uploaded to the device will be cached in libratbag + * and will be committed to it after a commit only. + */ + RATBAG_PROFILE_CAP_WRITABLE_NAME = 100, }; /** * @ingroup profile @@ -688,6 +696,31 @@ int ratbag_profile_has_capability(const struct ratbag_profile *profile, enum ratbag_profile_capability cap); +/** + * @ingroup profile + * + * Return the ratbag profile name. + * + * @param profile A previously initialized ratbag profile + * @return the profile name + */ +const char * +ratbag_profile_get_name(struct ratbag_profile *profile); + +/** + * @ingroup profile + * + * Set the name of a ratbag profile. + * + * @param profile A previously initialized ratbag profile + * @param name the profile name + * + * @return 0 on success or an error code otherwise + */ +int +ratbag_profile_set_name(struct ratbag_profile *profile, + const char *name); + /** * @ingroup profile * diff --git a/src/libratbag.sym b/src/libratbag.sym index 7235ee5d..6020a2a5 100644 --- a/src/libratbag.sym +++ b/src/libratbag.sym @@ -59,6 +59,7 @@ global: ratbag_led_unref; ratbag_device_get_num_leds; ratbag_profile_get_button; + ratbag_profile_get_name; ratbag_profile_get_num_resolutions; ratbag_profile_get_resolution; ratbag_profile_get_user_data; @@ -69,6 +70,7 @@ global: ratbag_profile_set_user_data; ratbag_profile_set_active; ratbag_profile_set_enabled; + ratbag_profile_set_name; ratbag_profile_unref; ratbag_resolution_get_dpi; ratbag_resolution_get_dpi_maximum; diff --git a/tools/ratbagctl.in b/tools/ratbagctl.in index fd65927f..c81b9229 100755 --- a/tools/ratbagctl.in +++ b/tools/ratbagctl.in @@ -27,7 +27,7 @@ import evdev import sys import argparse -from ratbagd import Ratbagd, RatbagdDevice, RatbagdProfile, RatbagdResolution, RatbagdButton, RatbagdLed, RatbagdDBusUnavailable # NOQA +from ratbagd import Ratbagd, RatbagdDevice, RatbagdProfile, RatbagdResolution, RatbagdButton, RatbagdLed, RatbagdDBusUnavailable, RatbagError, RatbagErrorCapability # NOQA button_specials_strmap = { RatbagdButton.ACTION_SPECIAL_UNKNOWN: "unknown", @@ -248,6 +248,7 @@ def print_profile(d, p, level): " (disabled)" if not p.enabled else "", " (active)" if p.is_active else "")) if p.enabled: + print(" " * level + "Name : '{}'".format(p.name)) print(" " * level + "Resolutions:") for r in p.resolutions: print_resolution(d, p, r, level + 2) @@ -495,6 +496,19 @@ def func_profile_get(r, args): print_profile(d, p, 0) +def func_profile_name_get(r, args): + p, d = find_profile(r, args) + print(p.name) + + +def func_profile_name_set(r, args): + p, d = find_profile(r, args) + if RatbagdProfile.CAP_WRITABLE_NAME not in p.capabilities: + raise RatbagErrorCapability("assigning a profile name is not supported on this profile") + p.name = args.name + d.commit() + + def func_profile_active_get(r, args): d = find_device(r, args) for p in d.profiles: @@ -697,6 +711,33 @@ parser_def = [ help_str: 'Show selected profile information', func: func_profile_get, }, + { + of_type: switch, + name: 'name', + help_str: 'access profile name information', + switch: [ + { + of_type: command, + name: 'get', + help_str: 'Show the name of the profile', + func: func_profile_name_get, + }, + { + of_type: command, + name: 'set', + help_str: 'Set the name of the profile', + pos_args: [ + { + of_type: argument, + name: 'name', + metavar: 'blah', + help_str: 'The name to set', + }, + ], + func: func_profile_name_set, + }, + ], + }, { of_type: command, name: 'enable', diff --git a/tools/ratbagctl.test.in b/tools/ratbagctl.test.in index 4c859874..5d8d2799 100755 --- a/tools/ratbagctl.test.in +++ b/tools/ratbagctl.test.in @@ -97,6 +97,11 @@ class TestRatbagCtl(unittest.TestCase): self.assertNotEqual(returncode, 0) return stdout + def launch_fail_with_exception_test(self, params, exception): + with self.assertRaises(exception): + returncode, stdout, stderr = self.run_ratbagctl(params) + return '' + @classmethod def setProfile(cls, profile): cls.run_ratbagctl("Profile active set {} test_device".format(profile)) @@ -130,6 +135,29 @@ class TestRatbgagCtlProfile(TestRatbagCtl): super(TestRatbgagCtlProfile, cls).setUpClass() cls.setProfile(0) + def test_profile_name_get(self): + command = "profile 0 name get" + r = self.launch_good_test(command + " test_device") + self.assertEqual(r, 'Profile 1') + self.launch_fail_test(command) + self.launch_fail_test(command + " X test_device") + self.launch_fail_test("profile name get test_device") + + def test_profile_name_set(self): + r = self.launch_good_test("profile 1 name get test_device") + self.assertEqual(r, 'Profile 2') + self.launch_good_test("profile 1 name set banana test_device") + r = self.launch_good_test("profile 1 name get test_device") + self.assertEqual(r, 'banana') + # profile 0 doesn't have the capability RATBAG_PROFILE_CAP_WRITABLE_NAME + self.launch_fail_with_exception_test("profile 0 name set kiwi test_device", toolbox.RatbagErrorCapability) + # better be safe than sorry, checking that the previous actually failed :) + r = self.launch_good_test("profile 0 name get test_device") + self.assertNotEqual(r, 'kiwi') + self.launch_fail_test("profile 1 name set blah") + self.launch_fail_test("profile 1 name set blah X test_device") + self.launch_fail_test("profile name set blah test_device") + def test_profile_active_get(self): command = "profile active get" r = self.launch_good_test(command + " test_device") diff --git a/tools/ratbagd.py b/tools/ratbagd.py index 2da9b8a0..162685e9 100644 --- a/tools/ratbagd.py +++ b/tools/ratbagd.py @@ -289,6 +289,8 @@ class RatbagdDevice(_RatbagdDBus): class RatbagdProfile(_RatbagdDBus): """Represents a ratbagd profile.""" + CAP_WRITABLE_NAME = 100 + def __init__(self, object_path): _RatbagdDBus.__init__(self, "Profile", object_path) @@ -297,12 +299,24 @@ class RatbagdProfile(_RatbagdDBus): """The capabilities of this profile as an array. Capabilities not present on the profile are not in the list. Thus use e.g. - if RatbagdPorfile.CAP_WRITABLE_NAME is in profile.capabilities: + if RatbagdProfile.CAP_WRITABLE_NAME is in profile.capabilities: do something """ return self._get_dbus_property("Capabilities") @GObject.property + def name(self): + """The name of the profile""" + return self._get_dbus_property("Name") + + @name.setter + def name(self, name): + """Set the name of this profile. + + @param name The new name, as str""" + self._set_dbus_property("Name", "s", name) + + @GObject.Property def index(self): """The index of this profile.""" return self._get_dbus_property("Index") diff --git a/tools/toolbox.py b/tools/toolbox.py index cd826e36..a66c847a 100644 --- a/tools/toolbox.py +++ b/tools/toolbox.py @@ -100,7 +100,7 @@ def terminate_ratbagd(ratbagd): ratbagctl = import_non_standard_path(RATBAGCTL_NAME, RATBAGCTL_PATH) -from ratbagctl import open_ratbagd, get_parser +from ratbagctl import open_ratbagd, get_parser, RatbagError, RatbagErrorCapability __all__ = [ RATBAGCTL_NAME, @@ -112,4 +112,6 @@ __all__ = [ terminate_ratbagd, open_ratbagd, get_parser, + RatbagError, + RatbagErrorCapability, ]