diff --git a/src/conf-tweaks/README.md b/src/conf-tweaks/README.md index 468c415d6daf4e89b59d152abdc0a38d03a791d5..dfb2a6410f08a44023f6ed05fd554dbaaa82b685 100644 --- a/src/conf-tweaks/README.md +++ b/src/conf-tweaks/README.md @@ -32,3 +32,16 @@ first one found will be used. The gtype option is used to define which type the gsetting is in case it's different than the type of the widget. This is mainly useful when things are remapped. + +### xresources + +```yaml +backend: xresources +key: dwm.background +type: color +default: #BBBBBB +``` + +Reads and writes to ~/.Xresources. If a given key is available in that file, it +will be read on program startup, otherwise the default value from the setting +definition is used. diff --git a/src/conf-tweaks/backends/meson.build b/src/conf-tweaks/backends/meson.build index 3a36b9c702e5bd3f40fbf35ad926763a32b32d03..ad8265bc4de44a1827dddc69aafbb6a91ddd02fb 100644 --- a/src/conf-tweaks/backends/meson.build +++ b/src/conf-tweaks/backends/meson.build @@ -1 +1,6 @@ -backend_sources = files('ms-tweaks-backend-gsettings.c', 'ms-tweaks-backend-gsettings.h') +backend_sources = files( + 'ms-tweaks-backend-gsettings.c', + 'ms-tweaks-backend-gsettings.h', + 'ms-tweaks-backend-xresources.c', + 'ms-tweaks-backend-xresources.h', +) diff --git a/src/conf-tweaks/backends/ms-tweaks-backend-xresources.c b/src/conf-tweaks/backends/ms-tweaks-backend-xresources.c new file mode 100644 index 0000000000000000000000000000000000000000..b8979816c7e96a1fa4055cd17cdbc612acd1168f --- /dev/null +++ b/src/conf-tweaks/backends/ms-tweaks-backend-xresources.c @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2025 Stefan Hansson + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Stefan Hansson + */ + +#define G_LOG_DOMAIN "ms-tweaks-backend-xresources" + +#include "ms-tweaks-backend-xresources.h" + +#include "../ms-tweaks-gtk-utils.h" +#include "../ms-tweaks-utils.h" + +#include + + +struct _MsTweaksBackendXresources { + MsTweaksBackendInterface parent_interface; + + const MsTweaksSetting *setting_data; + + const char *key; + + char *xresources_path; +}; + + +static char * +ms_tweaks_backend_xresources_get_xresources_path (MsTweaksBackendXresources *self) +{ + GPathBuf *xresources_path; + + if (self->xresources_path) + xresources_path = g_path_buf_new_from_path (self->xresources_path); + else { + xresources_path = g_path_buf_new_from_path (g_get_home_dir ()); + g_path_buf_push (xresources_path, ".Xresources"); + } + + return g_path_buf_free_to_path (xresources_path); +} + + +void +ms_tweaks_backend_xresources_set_xresources_path (MsTweaksBackend *backend, char *xresources_path) +{ + MsTweaksBackendXresources *self = MS_TWEAKS_BACKEND_XRESOURCES (backend); + self->xresources_path = xresources_path; +} + + +static GValue * +ms_tweaks_backend_xresources_get_value (MsTweaksBackend *backend) +{ + MsTweaksBackendXresources *self = MS_TWEAKS_BACKEND_XRESOURCES (backend); + g_autoptr (GDataInputStream) input_stream = NULL; + g_autoptr (GFileInputStream) file_input_stream; + g_autofree char *xresources_path; + g_autoptr (GError) error = NULL; + g_autoptr (GFile) input_file; + g_autofree char *line = NULL; + GValue *value; + + xresources_path = ms_tweaks_backend_xresources_get_xresources_path (self); + input_file = g_file_new_for_path (xresources_path); + file_input_stream = g_file_read (input_file, NULL, &error); + + value = g_new0 (GValue, 1); + g_value_init (value, G_TYPE_STRING); + g_value_set_string (value, self->setting_data->default_); + + if (!file_input_stream) { + /* Only warn once for "No such file or directory" as it otherwise may get printed many times. */ + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + g_warning_once ("Failed to read: %s", error->message); + else + ms_tweaks_warning (self->setting_data->name, "Failed to read: %s", error->message); + + return value; + } + + input_stream = g_data_input_stream_new (G_INPUT_STREAM (file_input_stream)); + + while ((line = g_data_input_stream_read_line (input_stream, NULL, NULL, &error)) != NULL) { + if (!error) { + char *result = g_strstr_len (line, -1, self->key); + + if (line == result) { + /* We found a line where the start matches our key. */ + g_auto (GStrv) key_value_pair = g_strsplit (line, ": ", 2); + + if (g_strv_length (key_value_pair) == 2) { + g_strstrip (key_value_pair[1]); + g_value_set_string (value, key_value_pair[1]); + break; + } else { + ms_tweaks_warning (self->setting_data->name, + "Malformed matching line skipped: %s", + line); + } + } + + g_free (line); + } else { + ms_tweaks_warning (self->setting_data->name, "Error while reading: %s", error->message); + + g_clear_error (&error); + } + } + + return value; +} + + +static void +rewrite_existing_xresources (MsTweaksBackendXresources *self, + const char *xresources_contents, + const char *xresources_path, + const char *new_value, + GError **error) +{ + /* Reading the file another time just to find what value currently is there is pretty ugly, + * should be possible to do this in a better way. However, given that this only is done in the + * setter method (which isn't called in startup), I'm not sure it's worth optimising. */ + g_autofree GValue *old_value = ms_tweaks_backend_xresources_get_value (MS_TWEAKS_BACKEND (self)); + g_autoptr (GString) contents_string = g_string_new (xresources_contents); + g_autofree char *line_to_replace = g_strconcat (self->key, + ": ", + g_value_get_string (old_value), + NULL); + g_autofree char *line_to_insert = NULL; + + if (new_value) + line_to_insert = g_strconcat (self->key, ": ", new_value, NULL); + else + line_to_insert = g_strdup (""); /* Remove the entry by replacing it with an empty string. */ + + g_debug ("Rewriting existing Xresources at \"%s\"", xresources_path); + /* If there is more than one identical entry, something is weird anyway. */ + g_string_replace (contents_string, line_to_replace, line_to_insert, 1); + + if (new_value && g_str_equal (xresources_contents, contents_string->str)) { + /* No change happened from g_string_replace, which probably means that the property doesn't + * yet exist in Xresources. Append a new line with it, if we have a new colour to set. */ + + /* But first, append a newline to the end of the file if necessary. */ + if (contents_string->len != 0 && contents_string->str[contents_string->len - 1] != '\n') + g_string_append_c (contents_string, '\n'); + + g_string_append_printf (contents_string, "%s\n", line_to_insert); + } + + if (!g_file_set_contents (xresources_path, contents_string->str, -1, error)) + g_warning ("Error while writing to Xresources at \"%s\": %s", + xresources_path, + (*error)->message); + + g_value_unset (old_value); +} + + +static void +write_new_xresources (MsTweaksBackendXresources *self, + const char *xresources_path, + const char *new_value, + GError **error) +{ + g_autofree char *new_xresources = g_strconcat (self->key, ": ", new_value, NULL); + g_autofree char *xresources_dir_path = g_path_get_dirname (xresources_path); + + g_debug ("xresources doesn't exist at \"%s\", creating new one (error: %s)", + xresources_path, + (*error)->message); + g_clear_error (error); + + if (g_mkdir_with_parents (xresources_dir_path, 0700) == -1) { + ms_tweaks_warning (self->setting_data->name, + "failed to create leading directories \"%s\": %s", + xresources_dir_path, + strerror (errno)); + return; + } + + if (!g_file_set_contents (xresources_path, new_xresources, -1, error)) { + ms_tweaks_warning (self->setting_data->name, + "error while writing to xresources at \"%s\": %s", + xresources_path, + (*error)->message); + } +} + + +static void +ms_tweaks_backend_xresources_set_value (MsTweaksBackend *backend, GValue *new_value_) +{ + MsTweaksBackendXresources *self = MS_TWEAKS_BACKEND_XRESOURCES (backend); + const char *new_value = new_value_ ? g_value_get_string (new_value_) : NULL; + g_autofree char *xresources_path = NULL; + g_autofree char *contents = NULL; + g_autoptr (GError) error = NULL; + + xresources_path = ms_tweaks_backend_xresources_get_xresources_path (self); + + if (!self->key) { + ms_tweaks_warning (self->setting_data->name, "key was NULL. Can't set property."); + return; + } + + if (g_file_get_contents (xresources_path, &contents, NULL, &error)) + rewrite_existing_xresources (self, contents, xresources_path, new_value, &error); + else + write_new_xresources (self, xresources_path, new_value, &error); +} + + +static const MsTweaksSetting * +ms_tweaks_backend_xresources_get_setting_data (MsTweaksBackend *backend) +{ + MsTweaksBackendXresources *self = MS_TWEAKS_BACKEND_XRESOURCES (backend); + + return self->setting_data; +} + + +static void +ms_tweaks_backend_xresources_interface_init (MsTweaksBackendInterface *iface) +{ + iface->get_value = ms_tweaks_backend_xresources_get_value; + iface->set_value = ms_tweaks_backend_xresources_set_value; + + iface->get_setting_data = ms_tweaks_backend_xresources_get_setting_data; +} + + +G_DEFINE_TYPE_WITH_CODE (MsTweaksBackendXresources, ms_tweaks_backend_xresources, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (MS_TYPE_TWEAKS_BACKEND, + ms_tweaks_backend_xresources_interface_init)) + + +static void +ms_tweaks_backend_xresources_init (MsTweaksBackendXresources *self) +{ +} + + +static void +ms_tweaks_backend_xresources_dispose (GObject *object) +{ + MsTweaksBackendXresources *self = MS_TWEAKS_BACKEND_XRESOURCES (object); + + g_clear_pointer (&self->xresources_path, g_free); +} + + +static void +ms_tweaks_backend_xresources_class_init (MsTweaksBackendXresourcesClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = ms_tweaks_backend_xresources_dispose; +} + + +MsTweaksBackend * +ms_tweaks_backend_xresources_new (const MsTweaksSetting *setting_data) +{ + MsTweaksBackendXresources *self = g_object_new (MS_TYPE_TWEAKS_BACKEND_XRESOURCES, NULL); + + self->setting_data = setting_data; + self->key = ms_tweaks_util_get_single_key (setting_data->key); + + return MS_TWEAKS_BACKEND (self); +} diff --git a/src/conf-tweaks/backends/ms-tweaks-backend-xresources.h b/src/conf-tweaks/backends/ms-tweaks-backend-xresources.h new file mode 100644 index 0000000000000000000000000000000000000000..7e6fd973aded8a6d971cadbd222f9603ca484e8d --- /dev/null +++ b/src/conf-tweaks/backends/ms-tweaks-backend-xresources.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2025 Stefan Hansson + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Stefan Hansson + */ + +#pragma once + +#include "ms-tweaks-backend-interface.h" +#include "ms-tweaks-parser.h" + +#include + +G_BEGIN_DECLS + +#define MS_TYPE_TWEAKS_BACKEND_XRESOURCES ms_tweaks_backend_xresources_get_type () +G_DECLARE_FINAL_TYPE (MsTweaksBackendXresources, ms_tweaks_backend_xresources, MS, TWEAKS_BACKEND_XRESOURCES, GObject) + +MsTweaksBackend *ms_tweaks_backend_xresources_new (const MsTweaksSetting *setting_data); + +void ms_tweaks_backend_xresources_set_xresources_path (MsTweaksBackend *backend, + char *xresources_path); + +G_END_DECLS diff --git a/src/conf-tweaks/ms-tweaks-preferences-page.c b/src/conf-tweaks/ms-tweaks-preferences-page.c index 5c900fb3cd64f3305ad5b84b385341963d2ec18d..b5700d924e2c1ffe3fd4684b6cd7189564592700 100644 --- a/src/conf-tweaks/ms-tweaks-preferences-page.c +++ b/src/conf-tweaks/ms-tweaks-preferences-page.c @@ -11,6 +11,7 @@ #include "ms-tweaks-preferences-page.h" #include "backends/ms-tweaks-backend-gsettings.h" +#include "backends/ms-tweaks-backend-xresources.h" #include "ms-tweaks-backend-interface.h" #include "ms-tweaks-callback-handlers.h" #include "ms-tweaks-mappings.h" @@ -384,10 +385,12 @@ ms_tweaks_preferences_page_initable_init (GInitable *initable, case MS_TWEAKS_BACKEND_IDENTIFIER_GSETTINGS: backend_state = ms_tweaks_backend_gsettings_new (setting_data); break; + case MS_TWEAKS_BACKEND_IDENTIFIER_XRESOURCES: + backend_state = ms_tweaks_backend_xresources_new (setting_data); + break; case MS_TWEAKS_BACKEND_IDENTIFIER_CSS: case MS_TWEAKS_BACKEND_IDENTIFIER_GTK3SETTINGS: case MS_TWEAKS_BACKEND_IDENTIFIER_SYSFS: - case MS_TWEAKS_BACKEND_IDENTIFIER_XRESOURCES: case MS_TWEAKS_BACKEND_IDENTIFIER_SOUNDTHEME: case MS_TWEAKS_BACKEND_IDENTIFIER_SYMLINK: default: diff --git a/src/conf-tweaks/ms-tweaks-utils.c b/src/conf-tweaks/ms-tweaks-utils.c index 46a341412648480610a5b12c236cc126fd3a97dd..e8e464c064990d7fd0bb76407ed93c6c23f5744f 100644 --- a/src/conf-tweaks/ms-tweaks-utils.c +++ b/src/conf-tweaks/ms-tweaks-utils.c @@ -61,6 +61,25 @@ ms_tweaks_get_filename_extension (const char *filename) return dot + 1; } +/** + * ms_tweaks_util_get_single_key: + * @key_array: Array to get the first element of. + * + * Returns: The key if the array is 1 element long, otherwise NULL. + */ +const char * +ms_tweaks_util_get_single_key (const GPtrArray *key_array) +{ + const char *key = NULL; + + if (key_array->len == 1) + key = g_ptr_array_index (key_array, 0); + else + g_warning ("Only single-element key values are allowed"); + + return key; +} + /** * ms_tweaks_util_get_key_by_value_string: * @hash_table: The GHashTable to find the key in. diff --git a/src/conf-tweaks/ms-tweaks-utils.h b/src/conf-tweaks/ms-tweaks-utils.h index 55bdb829d003ba41abfce7b45336731a55f81703..75c2dea57a86c48fecd9e48d9614e77c05f2d8ff 100644 --- a/src/conf-tweaks/ms-tweaks-utils.h +++ b/src/conf-tweaks/ms-tweaks-utils.h @@ -15,6 +15,7 @@ const char *ms_tweaks_util_boolean_to_string (const gboolean value); const char *ms_tweaks_get_filename_extension (const char *filename); /* Value retrieval functions. */ +const char *ms_tweaks_util_get_single_key (const GPtrArray *key_array); char *ms_tweaks_util_get_key_by_value_string (GHashTable *hash_table, const char *value_to_find); gboolean ms_tweaks_util_string_to_boolean (const char *string); diff --git a/tests/meson.build b/tests/meson.build index 29cfd557d80aa1a7bc43168337f20332ab2cac51..1de10bd6cbed2a57540cdd64170ac77429eacc78 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -34,6 +34,7 @@ if phoc.found() unit_tests = [ 'tweaks-backend-gsettings', + 'tweaks-backend-xresources', 'tweaks-datasources', 'tweaks-gtk-utils', 'tweaks-mappings', diff --git a/tests/test-tweaks-backend-common.h b/tests/test-tweaks-backend-common.h index 3b53c5470477adc9706fe14973f383f18c478729..a03935a6c37b979630f326f44bbf3ffe95986aa4 100644 --- a/tests/test-tweaks-backend-common.h +++ b/tests/test-tweaks-backend-common.h @@ -21,3 +21,49 @@ test_backend_fixture_teardown (BackendTestFixture *fixture, gconstpointer unused g_object_unref (fixture->backend); ms_tweaks_setting_free (fixture->setting); } + + +static void +test_construct (BackendTestFixture *fixture, gconstpointer unused) +{ + g_assert_nonnull (fixture->backend); +} + + +static void +test_get (BackendTestFixture *fixture, gconstpointer unused) +{ + g_autofree GValue *value = NULL; + + g_assert_nonnull (fixture->backend); + + value = MS_TWEAKS_BACKEND_GET_IFACE (fixture->backend)->get_value (fixture->backend); + + g_assert_nonnull (value); + + g_value_unset (value); +} + + +static void +test_set (BackendTestFixture *fixture, gconstpointer string_value) +{ + g_autofree GValue *value = g_new0 (GValue, 1); + g_value_init (value, G_TYPE_STRING); + g_value_set_string (value, string_value); + + g_assert_nonnull (fixture->backend); + + MS_TWEAKS_BACKEND_GET_IFACE (fixture->backend)->set_value (fixture->backend, value); + + g_value_unset (value); +} + + +static void +test_remove (BackendTestFixture *fixture, gconstpointer unused) +{ + g_assert_nonnull (fixture->backend); + + MS_TWEAKS_BACKEND_GET_IFACE (fixture->backend)->set_value (fixture->backend, NULL); +} diff --git a/tests/test-tweaks-backend-constants.h b/tests/test-tweaks-backend-constants.h new file mode 100644 index 0000000000000000000000000000000000000000..c115d85ea4f5e190fc0eb3ec4f9f523d19259a25 --- /dev/null +++ b/tests/test-tweaks-backend-constants.h @@ -0,0 +1 @@ +#define MS_TWEAKS_BACKEND_TEST_DIRECTORY "/tmp/ms-tweaks-tests" diff --git a/tests/test-tweaks-backend-gsettings.c b/tests/test-tweaks-backend-gsettings.c index 5d28fae0092e502c607bbe7e04370bc9e51b841b..56f6fa9a79e008e3dcbebf7d51f3fdbe4a13e55b 100644 --- a/tests/test-tweaks-backend-gsettings.c +++ b/tests/test-tweaks-backend-gsettings.c @@ -43,43 +43,6 @@ test_gsettings_fixture_setup_alternative (BackendTestFixture *fixture, gconstpoi } -static void -test_construct (BackendTestFixture *fixture, gconstpointer unused) -{ - g_assert_true (fixture->backend); -} - - -static void -test_get (BackendTestFixture *fixture, gconstpointer unused) -{ - g_autofree GValue *value = NULL; - - g_assert_true (fixture->backend); - - value = MS_TWEAKS_BACKEND_GET_IFACE (fixture->backend)->get_value (fixture->backend); - - g_assert_true (value); - - g_value_unset (value); -} - - -static void -test_set (BackendTestFixture *fixture, gconstpointer unused) -{ - g_autofree GValue *value = g_new0 (GValue, 1); - g_value_init (value, G_TYPE_STRING); - g_value_set_string (value, "prefer-light"); - - g_assert_true (fixture->backend); - - MS_TWEAKS_BACKEND_GET_IFACE (fixture->backend)->set_value (fixture->backend, value); - - g_value_unset (value); -} - - static void test_set_alternative (BackendTestFixture *fixture, gconstpointer unused) { @@ -95,12 +58,12 @@ test_set_alternative (BackendTestFixture *fixture, gconstpointer unused) } -#define BACKEND_TEST_ADD(name, test_func) g_test_add ((name), \ - BackendTestFixture, \ - NULL, \ - test_gsettings_fixture_setup, \ - (test_func), \ - test_backend_fixture_teardown) +#define BACKEND_TEST_ADD(name, string_value, test_func) g_test_add ((name), \ + BackendTestFixture, \ + (string_value), \ + test_gsettings_fixture_setup, \ + (test_func), \ + test_backend_fixture_teardown) #define BACKEND_TEST_ADD_ALT(name, test_func) g_test_add ((name), \ @@ -117,15 +80,21 @@ main (int argc, char *argv[]) g_test_init (&argc, &argv, NULL); BACKEND_TEST_ADD ("/phosh-mobile-settings/test-tweaks-backend-gsettings-construct", + NULL, test_construct); BACKEND_TEST_ADD ("/phosh-mobile-settings/test-tweaks-backend-gsettings-get", + NULL, test_get); BACKEND_TEST_ADD_ALT ("/phosh-mobile-settings/test-tweaks-backend-gsettings-alternative", test_get); BACKEND_TEST_ADD ("/phosh-mobile-settings/test-tweaks-backend-gsettings-set", + "prefer-light", test_set); BACKEND_TEST_ADD_ALT ("/phosh-mobile-settings/test-tweaks-backend-gsettings-set-alternative", test_set_alternative); + BACKEND_TEST_ADD ("/phosh-mobile-settings/test-tweaks-backend-gsettings-remove", + NULL, + test_remove); return g_test_run (); } diff --git a/tests/test-tweaks-backend-xresources.c b/tests/test-tweaks-backend-xresources.c new file mode 100644 index 0000000000000000000000000000000000000000..4397a81ec12c863db1f900499deaf3dee9b230f8 --- /dev/null +++ b/tests/test-tweaks-backend-xresources.c @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2025 Stefan Hansson + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Stefan Hansson + */ + +#include "test-tweaks-backend-constants.h" + +#include "conf-tweaks/backends/ms-tweaks-backend-xresources.h" +#include "test-tweaks-backend-common.h" + + +static void +test_xresources_fixture_setup (BackendTestFixture *fixture, gconstpointer unused) +{ + fixture->setting = g_new0 (MsTweaksSetting, 1); + fixture->setting->name = g_strdup ("Dwm background or something idk"); + fixture->setting->key = g_ptr_array_new_full (1, g_free); + g_ptr_array_add (fixture->setting->key, g_strdup ("dwm.background")); + + fixture->backend = ms_tweaks_backend_xresources_new (fixture->setting); + ms_tweaks_backend_xresources_set_xresources_path (fixture->backend, + g_strdup (MS_TWEAKS_BACKEND_TEST_DIRECTORY"/.Xresources")); +} + + +#define BACKEND_TEST_ADD(name, string_value, test_func) g_test_add ((name), \ + BackendTestFixture, \ + string_value, \ + test_xresources_fixture_setup, \ + (test_func), \ + test_backend_fixture_teardown) + + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + BACKEND_TEST_ADD ("/phosh-mobile-settings/test-tweaks-backend-xresources-construct", + NULL, + test_construct); + BACKEND_TEST_ADD ("/phosh-mobile-settings/test-tweaks-backend-xresources-set", + "#005577", + test_set); + BACKEND_TEST_ADD ("/phosh-mobile-settings/test-tweaks-backend-xresources-get", + NULL, + test_get); + BACKEND_TEST_ADD ("/phosh-mobile-settings/test-tweaks-backend-xresources-remove", + NULL, + test_remove); + + return g_test_run (); +}