From f94a8edb0edcca8af53b3994d23735df6a5974f1 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Thu, 24 Nov 2022 13:50:27 +1000 Subject: Add support for custom pointer acceleration Adds new properties and xorg.conf entries for setting the acceleration function's points and step. `AccelProfile` option can now accept `custom` value. Add 4 new options which only apply when `AccelProfile` is `custom`: - Add `AccelPointsFallback` option for setting the points of the Fallback acceleration function. Points values are represented by a space-separated list, e.g. "0.0 1.0 2.4 2.5". - Add `AccelStepFallback` option for setting the step of the Fallback acceleration function. When a step of 0.0 is provided, libinput default Fallback acceleration function is used. - Add `AccelPointsMotion` and `AccelStepMotion` options, which are equivalent to `AccelPointsFallback` and `AccelStepFallback` options, but apply to the Motion acceleration function. See libinput documentation for a detailed explanation of custom pointer acceleration. --- configure.ac | 11 + include/libinput-properties.h | 18 +- man/libinput.man | 19 +- meson.build | 6 +- src/Makefile.am | 7 +- src/util-macros.h | 65 ++++++ src/util-strings.c | 243 ++++++++++++++++++++++ src/util-strings.h | 464 ++++++++++++++++++++++++++++++++++++++++++ src/xf86libinput.c | 375 ++++++++++++++++++++++++++++++++-- 9 files changed, 1187 insertions(+), 21 deletions(-) create mode 100644 src/util-macros.h create mode 100644 src/util-strings.c create mode 100644 src/util-strings.h diff --git a/configure.ac b/configure.ac index db506ed..3b34b8e 100644 --- a/configure.ac +++ b/configure.ac @@ -78,6 +78,17 @@ AC_LINK_IFELSE( [AC_MSG_RESULT([no]) [libinput_have_axis_value_v120=no]]) +AC_MSG_CHECKING([if libinput_config_accel_create is available]) +AC_LINK_IFELSE( + [AC_LANG_PROGRAM([[#include ]], + [[libinput_config_accel_create(0)]])], + [AC_MSG_RESULT([yes]) + AC_DEFINE(HAVE_LIBINPUT_CUSTOM_ACCEL, [1], + [libinput_config_accel_create() is available]) + [libinput_have_custom_accel=yes]], + [AC_MSG_RESULT([no]) + [libinput_have_custom_accel=no]]) + LIBS=$OLD_LIBS CFLAGS=$OLD_CFLAGS diff --git a/include/libinput-properties.h b/include/libinput-properties.h index 1655771..004dac9 100644 --- a/include/libinput-properties.h +++ b/include/libinput-properties.h @@ -63,18 +63,30 @@ /* Pointer accel speed: FLOAT, 1 value, 32 bit, read-only*/ #define LIBINPUT_PROP_ACCEL_DEFAULT "libinput Accel Speed Default" -/* Pointer accel profile: BOOL, 2 values in order adaptive, flat, +/* Pointer accel profile: BOOL, 3 values in order adaptive, flat, custom * only one is enabled at a time at max, read-only */ #define LIBINPUT_PROP_ACCEL_PROFILES_AVAILABLE "libinput Accel Profiles Available" -/* Pointer accel profile: BOOL, 2 values in order adaptive, flat, +/* Pointer accel profile: BOOL, 3 values in order adaptive, flat, custom only one is enabled at a time at max, read-only */ #define LIBINPUT_PROP_ACCEL_PROFILE_ENABLED_DEFAULT "libinput Accel Profile Enabled Default" -/* Pointer accel profile: BOOL, 2 values in order adaptive, flat, +/* Pointer accel profile: BOOL, 3 values in order adaptive, flat, custom only one is enabled at a time at max */ #define LIBINPUT_PROP_ACCEL_PROFILE_ENABLED "libinput Accel Profile Enabled" +/* Points for the custom accel profile: FLOAT, N values */ +#define LIBINPUT_PROP_ACCEL_CUSTOM_POINTS_FALLBACK "libinput Accel Custom Fallback Points" + +/* Steps for the custom accel profile: FLOAT, 1 value */ +#define LIBINPUT_PROP_ACCEL_CUSTOM_STEP_FALLBACK "libinput Accel Custom Fallback Step" + +/* Points for the custom accel profile: FLOAT, N values */ +#define LIBINPUT_PROP_ACCEL_CUSTOM_POINTS_MOTION "libinput Accel Custom Motion Points" + +/* Steps for the custom accel profile: FLOAT, 1 value */ +#define LIBINPUT_PROP_ACCEL_CUSTOM_STEP_MOTION "libinput Accel Custom Motion Step" + /* Natural scrolling: BOOL, 1 value */ #define LIBINPUT_PROP_NATURAL_SCROLL "libinput Natural Scrolling Enabled" diff --git a/man/libinput.man b/man/libinput.man index a111da0..689afa4 100644 --- a/man/libinput.man +++ b/man/libinput.man @@ -45,13 +45,28 @@ are supported: Sets the pointer acceleration profile to the given profile. Permitted values are .BI adaptive, -.BI flat. +.BI flat, +.BI custom. Not all devices support this option or all profiles. If a profile is unsupported, the default profile for this device is used. For a description on the profiles and their behavior, see the libinput documentation. .TP 7 .BI "Option \*qAccelSpeed\*q \*q" float \*q -Sets the pointer acceleration speed within the range [-1, 1] +Sets the pointer acceleration speed within the range [-1, 1]. +This only applies to the flat or adaptive profile. +.BI "Option \*AccelPointsFallback\*q \*q" string \*q +Sets the points of the Fallback acceleration function, (see the libinput documentation). +The string must be a space-separated list of floating point non-negative numbers, e.g. +"0.0 1.0 2.4 2.5". +This only applies to the custom profile. +.BI "Option \*AccelStepFallback\*q \*q" float \*q +Sets the step between the points of the Fallback acceleration function, (see the libinput documentation). +When a step of 0.0 is provided, libinput's default Fallback acceleration function is used. +This only applies to the custom profile. +.BI "Option \*AccelPointsMotion\*q \*q" string \*q +Equivalent to AccelPointsFallback but applies to the Motion acceleration function. +.BI "Option \*AccelStepMotion\*q \*q" float \*q +Equivalent to AccelStepFallback but applies to the Motion acceleration function. .TP 7 .BI "Option \*qButtonMapping\*q \*q" string \*q Sets the logical button mapping for this device, see diff --git a/meson.build b/meson.build index 2a5a639..cb0926f 100644 --- a/meson.build +++ b/meson.build @@ -53,6 +53,10 @@ if cc.has_function('libinput_event_pointer_get_scroll_value_v120', dependencies: dep_libinput) config_h.set('HAVE_LIBINPUT_AXIS_VALUE_V120', 1) endif +if cc.has_function('libinput_config_accel_create', + dependencies: dep_libinput) + config_h.set('HAVE_LIBINPUT_CUSTOM_ACCEL', 1) +endif dir_headers = get_option('sdkdir') if dir_headers == '' @@ -84,7 +88,7 @@ dep_drivers = [ dep_libdraglock, ] -driver_src = ['src/xf86libinput.c'] +driver_src = ['src/xf86libinput.c', 'src/util-strings.c'] driver_lib = shared_module( 'libinput_drv', driver_src, diff --git a/src/Makefile.am b/src/Makefile.am index 5dcb55e..44d3a9c 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -33,7 +33,12 @@ AM_CPPFLAGS =-I$(top_srcdir)/include $(LIBINPUT_CFLAGS) @DRIVER_NAME@_drv_la_LIBADD = $(LIBINPUT_LIBS) libdraglock.la libbezier.la -lm @DRIVER_NAME@_drv_ladir = @inputdir@ -@DRIVER_NAME@_drv_la_SOURCES = xf86libinput.c +@DRIVER_NAME@_drv_la_SOURCES = \ + xf86libinput.c \ + util-macros.h \ + util-strings.h \ + util-strings.c \ + $(NULL) noinst_LTLIBRARIES = libdraglock.la libbezier.la libdraglock_la_SOURCES = draglock.c draglock.h diff --git a/src/util-macros.h b/src/util-macros.h new file mode 100644 index 0000000..d56a8a3 --- /dev/null +++ b/src/util-macros.h @@ -0,0 +1,65 @@ +/* + * Copyright © 2008-2011 Kristian Høgsberg + * Copyright © 2011 Intel Corporation + * Copyright © 2013-2015 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#pragma once + +#include "config.h" + +#define ARRAY_LENGTH(a) (sizeof (a) / sizeof (a)[0]) +/** + * Iterate through the array _arr, assigning the variable elem to each + * element. elem only exists within the loop. + */ +#define ARRAY_FOR_EACH(_arr, _elem) \ + for (__typeof__((_arr)[0]) *_elem = _arr; \ + _elem < (_arr) + ARRAY_LENGTH(_arr); \ + _elem++) + +#define min(a, b) (((a) < (b)) ? (a) : (b)) +#define max(a, b) (((a) > (b)) ? (a) : (b)) + +#define ANSI_HIGHLIGHT "\x1B[0;1;39m" +#define ANSI_RED "\x1B[0;31m" +#define ANSI_GREEN "\x1B[0;32m" +#define ANSI_YELLOW "\x1B[0;33m" +#define ANSI_BLUE "\x1B[0;34m" +#define ANSI_MAGENTA "\x1B[0;35m" +#define ANSI_CYAN "\x1B[0;36m" +#define ANSI_BRIGHT_RED "\x1B[0;31;1m" +#define ANSI_BRIGHT_GREEN "\x1B[0;32;1m" +#define ANSI_BRIGHT_YELLOW "\x1B[0;33;1m" +#define ANSI_BRIGHT_BLUE "\x1B[0;34;1m" +#define ANSI_BRIGHT_MAGENTA "\x1B[0;35;1m" +#define ANSI_BRIGHT_CYAN "\x1B[0;36;1m" +#define ANSI_NORMAL "\x1B[0m" + +#define ANSI_UP "\x1B[%dA" +#define ANSI_DOWN "\x1B[%dB" +#define ANSI_RIGHT "\x1B[%dC" +#define ANSI_LEFT "\x1B[%dD" + +#define CASE_RETURN_STRING(a) case a: return #a + +#define _fallthrough_ __attribute__((fallthrough)) diff --git a/src/util-strings.c b/src/util-strings.c new file mode 100644 index 0000000..d0a3fa0 --- /dev/null +++ b/src/util-strings.c @@ -0,0 +1,243 @@ +/* + * Copyright © 2008 Kristian Høgsberg + * Copyright © 2013-2015 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "config.h" + +#include "util-strings.h" + +/** + * Return the next word in a string pointed to by state before the first + * separator character. Call repeatedly to tokenize a whole string. + * + * @param state Current state + * @param len String length of the word returned + * @param separators List of separator characters + * + * @return The first word in *state, NOT null-terminated + */ +static const char * +next_word(const char **state, size_t *len, const char *separators) +{ + const char *next = *state; + size_t l; + + if (!*next) + return NULL; + + next += strspn(next, separators); + if (!*next) { + *state = next; + return NULL; + } + + l = strcspn(next, separators); + *state = next + l; + *len = l; + + return next; +} + +/** + * Return a null-terminated string array with the contents of argv + * duplicated. + * + * Use strv_free() to free the array. + * + * @return A null-terminated string array or NULL on errors + */ +char** +strv_from_argv(int argc, char **argv) +{ + char **strv = NULL; + + assert(argc >= 0); + + if (argc == 0) + return NULL; + + strv = zalloc((argc + 1) * sizeof *strv); + for (int i = 0; i < argc; i++) { + char *copy = safe_strdup(argv[i]); + if (!copy) { + strv_free(strv); + return NULL; + } + strv[i] = copy; + } + return strv; +} + +/** + * Return a null-terminated string array with the tokens in the input + * string, e.g. "one two\tthree" with a separator list of " \t" will return + * an array [ "one", "two", "three", NULL ] and num elements 3. + * + * Use strv_free() to free the array. + * + * Another example: + * result = strv_from_string("+1-2++3--4++-+5-+-", "+-", &nelem) + * result == [ "1", "2", "3", "4", "5", NULL ] and nelem == 5 + * + * @param in Input string + * @param separators List of separator characters + * @param num_elements Number of elements found in the input string + * + * @return A null-terminated string array or NULL on errors + */ +char ** +strv_from_string(const char *in, const char *separators, size_t *num_elements) +{ + assert(in != NULL); + + const char *s = in; + size_t l, nelems = 0; + while (next_word(&s, &l, separators) != NULL) + nelems++; + + if (nelems == 0) { + *num_elements = 0; + return NULL; + } + + size_t strv_len = nelems + 1; /* NULL-terminated */ + char **strv = zalloc(strv_len * sizeof *strv); + + size_t idx = 0; + const char *word; + s = in; + while ((word = next_word(&s, &l, separators)) != NULL) { + char *copy = strndup(word, l); + if (!copy) { + strv_free(strv); + *num_elements = 0; + return NULL; + } + + strv[idx++] = copy; + } + + *num_elements = nelems; + + return strv; +} + +/** + * Return a newly allocated string with all elements joined by the + * joiner, same as Python's string.join() basically. + * A strv of ["one", "two", "three", NULL] with a joiner of ", " results + * in "one, two, three". + * + * An empty strv ([NULL]) returns NULL, same for passing NULL as either + * argument. + * + * @param strv Input string array + * @param joiner Joiner between the elements in the final string + * + * @return A null-terminated string joining all elements + */ +char * +strv_join(char **strv, const char *joiner) +{ + char **s; + char *str; + size_t slen = 0; + size_t count = 0; + + if (!strv || !joiner) + return NULL; + + if (strv[0] == NULL) + return NULL; + + for (s = strv, count = 0; *s; s++, count++) { + slen += strlen(*s); + } + + assert(slen < 1000); + assert(strlen(joiner) < 1000); + assert(count > 0); + assert(count < 100); + + slen += (count - 1) * strlen(joiner); + + str = zalloc(slen + 1); /* trailing \0 */ + for (s = strv; *s; s++) { + strcat(str, *s); + --count; + if (count > 0) + strcat(str, joiner); + } + + return str; +} + +/** + * Return a pointer to the basename within filename. + * If the filename the empty string or a directory (i.e. the last char of + * filename is '/') NULL is returned. + */ +const char * +safe_basename(const char *filename) +{ + const char *basename; + + if (*filename == '\0') + return NULL; + + basename = strrchr(filename, '/'); + if (basename == NULL) + return filename; + + if (*(basename + 1) == '\0') + return NULL; + + return basename + 1; +} + +/** + * Similar to basename() but returns the trunk only without the (last) + * trailing suffix, so that: + * + * - foo.c returns foo + * - foo.a.b returns foo.a + * - foo returns foo + * - foo/ returns "" + * + * @return an allocated string representing the trunk name of the file + */ +char * +trunkname(const char *filename) +{ + const char *base = safe_basename(filename); + char *suffix; + + if (base == NULL) + return safe_strdup(""); + + suffix = rindex(base, '.'); + if (suffix == NULL) + return safe_strdup(base); + else + return strndup(base, suffix-base); +} diff --git a/src/util-strings.h b/src/util-strings.h new file mode 100644 index 0000000..dc68beb --- /dev/null +++ b/src/util-strings.h @@ -0,0 +1,464 @@ +/* + * Copyright © 2008 Kristian Høgsberg + * Copyright © 2013-2015 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#pragma once + +#include "config.h" +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef HAVE_LOCALE_H +#include +#endif +#ifdef HAVE_XLOCALE_H +#include +#endif + +#include "util-macros.h" + +static inline bool +streq(const char *str1, const char *str2) +{ + /* one NULL, one not NULL is always false */ + if (str1 && str2) + return strcmp(str1, str2) == 0; + return str1 == str2; +} + +static inline bool +strneq(const char *str1, const char *str2, int n) +{ + /* one NULL, one not NULL is always false */ + if (str1 && str2) + return strncmp(str1, str2, n) == 0; + return str1 == str2; +} + +static inline void * +zalloc(size_t size) +{ + void *p; + + /* We never need to alloc anything more than 1,5 MB so we can assume + * if we ever get above that something's going wrong */ + if (size > 1536 * 1024) + assert(!"bug: internal malloc size limit exceeded"); + + p = calloc(1, size); + if (!p) + abort(); + + return p; +} + +/** + * strdup guaranteed to succeed. If the input string is NULL, the output + * string is NULL. If the input string is a string pointer, we strdup or + * abort on failure. + */ +static inline char* +safe_strdup(const char *str) +{ + char *s; + + if (!str) + return NULL; + + s = strdup(str); + if (!s) + abort(); + return s; +} + +/** + * Simple wrapper for asprintf that ensures the passed in-pointer is set + * to NULL upon error. + * The standard asprintf() call does not guarantee the passed in pointer + * will be NULL'ed upon failure, whereas this wrapper does. + * + * @param strp pointer to set to newly allocated string. + * This pointer should be passed to free() to release when done. + * @param fmt the format string to use for printing. + * @return The number of bytes printed (excluding the null byte terminator) + * upon success or -1 upon failure. In the case of failure the pointer is set + * to NULL. + */ +__attribute__ ((format (printf, 2, 3))) +static inline int +xasprintf(char **strp, const char *fmt, ...) +{ + int rc = 0; + va_list args; + + va_start(args, fmt); + rc = vasprintf(strp, fmt, args); + va_end(args); + if ((rc == -1) && strp) + *strp = NULL; + + return rc; +} + +__attribute__ ((format (printf, 2, 0))) +static inline int +xvasprintf(char **strp, const char *fmt, va_list args) +{ + int rc = 0; + rc = vasprintf(strp, fmt, args); + if ((rc == -1) && strp) + *strp = NULL; + + return rc; +} + +static inline bool +safe_atoi_base(const char *str, int *val, int base) +{ + char *endptr; + long v; + + assert(base == 10 || base == 16 || base == 8); + + errno = 0; + v = strtol(str, &endptr, base); + if (errno > 0) + return false; + if (str == endptr) + return false; + if (*str != '\0' && *endptr != '\0') + return false; + + if (v > INT_MAX || v < INT_MIN) + return false; + + *val = v; + return true; +} + +static inline bool +safe_atoi(const char *str, int *val) +{ + return safe_atoi_base(str, val, 10); +} + +static inline bool +safe_atou_base(const char *str, unsigned int *val, int base) +{ + char *endptr; + unsigned long v; + + assert(base == 10 || base == 16 || base == 8); + + errno = 0; + v = strtoul(str, &endptr, base); + if (errno > 0) + return false; + if (str == endptr) + return false; + if (*str != '\0' && *endptr != '\0') + return false; + + if ((long)v < 0) + return false; + + *val = v; + return true; +} + +static inline bool +safe_atou(const char *str, unsigned int *val) +{ + return safe_atou_base(str, val, 10); +} + +static inline bool +safe_atod(const char *str, double *val) +{ + char *endptr; + double v; +#ifdef HAVE_LOCALE_H + locale_t c_locale; +#endif + size_t slen = strlen(str); + + /* We don't have a use-case where we want to accept hex for a double + * or any of the other values strtod can parse */ + for (size_t i = 0; i < slen; i++) { + char c = str[i]; + + if (isdigit(c)) + continue; + switch(c) { + case '+': + case '-': + case '.': + break; + default: + return false; + } + } + +#ifdef HAVE_LOCALE_H + /* Create a "C" locale to force strtod to use '.' as separator */ + c_locale = newlocale(LC_NUMERIC_MASK, "C", (locale_t)0); + if (c_locale == (locale_t)0) + return false; + + errno = 0; + v = strtod_l(str, &endptr, c_locale); + freelocale(c_locale); +#else + /* No locale support in provided libc, assume it already uses '.' */ + errno = 0; + v = strtod(str, &endptr); +#endif + if (errno > 0) + return false; + if (str == endptr) + return false; + if (*str != '\0' && *endptr != '\0') + return false; + if (v != 0.0 && !isnormal(v)) + return false; + + *val = v; + return true; +} + +char **strv_from_argv(int argc, char **argv); +char **strv_from_string(const char *in, const char *separator, size_t *num_elements); +char *strv_join(char **strv, const char *joiner); + +static inline void +strv_free(char **strv) { + char **s = strv; + + if (!strv) + return; + + while (*s != NULL) { + free(*s); + *s = (char*)0x1; /* detect use-after-free */ + s++; + } + + free (strv); +} + +/** + * parse a string containing a list of doubles into a double array. + * + * @param in string to parse + * @param separator string used to separate double in list e.g. "," + * @param result double array + * @param length length of double array + * @return true when parsed successfully otherwise false + */ +static inline double * +double_array_from_string(const char *in, + const char *separator, + size_t *length) +{ + double *result = NULL; + *length = 0; + + size_t nelem; + char **strv = strv_from_string(in, separator, &nelem); + if(!strv) + return result; + + double *numv = zalloc(sizeof(double) * nelem); + for (size_t idx = 0; idx < nelem; idx++) { + double val; + if (!safe_atod(strv[idx], &val)) + goto out; + + numv[idx] = val; + } + + result = numv; + numv = NULL; + *length = nelem; + +out: + strv_free(strv); + free(numv); + return result; +} + +struct key_value_str{ + char *key; + char *value; +}; + +struct key_value_double { + double key; + double value; +}; + +static inline ssize_t +kv_double_from_string(const char *string, + const char *pair_separator, + const char *kv_separator, + struct key_value_double **result_out) + +{ + struct key_value_double *result = NULL; + + if (!pair_separator || pair_separator[0] == '\0' || + !kv_separator || kv_separator[0] == '\0') + return -1; + + size_t npairs; + char **pairs = strv_from_string(string, pair_separator, &npairs); + if (!pairs || npairs == 0) + goto error; + + result = zalloc(npairs * sizeof *result); + + for (size_t idx = 0; idx < npairs; idx++) { + char *pair = pairs[idx]; + size_t nelem; + char **kv = strv_from_string(pair, kv_separator, &nelem); + double k, v; + + if (!kv || nelem != 2 || + !safe_atod(kv[0], &k) || + !safe_atod(kv[1], &v)) { + strv_free(kv); + goto error; + } + + result[idx].key = k; + result[idx].value = v; + + strv_free(kv); + } + + strv_free(pairs); + + *result_out = result; + + return npairs; + +error: + strv_free(pairs); + free(result); + return -1; +} + +/** + * Strip any of the characters in what from the beginning and end of the + * input string. + * + * @return a newly allocated string with none of "what" at the beginning or + * end of string + */ +static inline char * +strstrip(const char *input, const char *what) +{ + char *str, *last; + + str = safe_strdup(&input[strspn(input, what)]); + + last = str; + + for (char *c = str; *c != '\0'; c++) { + if (!strchr(what, *c)) + last = c + 1; + } + + *last = '\0'; + + return str; +} + +/** + * Return true if str ends in suffix, false otherwise. If the suffix is the + * empty string, strendswith() always returns false. + */ +static inline bool +strendswith(const char *str, const char *suffix) +{ + size_t slen = strlen(str); + size_t suffixlen = strlen(suffix); + size_t offset; + + if (slen == 0 || suffixlen == 0 || suffixlen > slen) + return false; + + offset = slen - suffixlen; + return strneq(&str[offset], suffix, suffixlen); +} + +static inline bool +strstartswith(const char *str, const char *prefix) +{ + size_t prefixlen = strlen(prefix); + + return prefixlen > 0 ? strneq(str, prefix, strlen(prefix)) : false; +} + +const char * +safe_basename(const char *filename); + +char * +trunkname(const char *filename); + +/** + * Return a copy of str with all % converted to %% to make the string + * acceptable as printf format. + */ +static inline char * +str_sanitize(const char *str) +{ + if (!str) + return NULL; + + if (!strchr(str, '%')) + return strdup(str); + + size_t slen = min(strlen(str), 512); + char *sanitized = zalloc(2 * slen + 1); + const char *src = str; + char *dst = sanitized; + + for (size_t i = 0; i < slen; i++) { + if (*src == '%') + *dst++ = '%'; + *dst++ = *src++; + } + *dst = '\0'; + + return sanitized; +} diff --git a/src/xf86libinput.c b/src/xf86libinput.c index 92e567d..fbe1e94 100644 --- a/src/xf86libinput.c +++ b/src/xf86libinput.c @@ -45,6 +45,7 @@ #include "bezier.h" #include "draglock.h" #include "libinput-properties.h" +#include "util-strings.h" #define TOUCHPAD_NUM_AXES 4 /* x, y, hscroll, vscroll */ #define TABLET_NUM_BUTTONS 7 /* we need scroll buttons */ @@ -54,6 +55,15 @@ #define TOUCHPAD_SCROLL_DIST_MIN 10 /* in libinput pixels */ #define TOUCHPAD_SCROLL_DIST_MAX 50 /* in libinput pixels */ +#if HAVE_LIBINPUT_CUSTOM_ACCEL +#define CUSTOM_ACCEL_NPOINTS_MIN 2 +#define CUSTOM_ACCEL_NPOINTS_MAX 64 +#define CUSTOM_ACCEL_POINT_MIN 0 +#define CUSTOM_ACCEL_POINT_MAX 10000 +#define CUSTOM_ACCEL_STEP_MIN 0 +#define CUSTOM_ACCEL_STEP_MAX 10000 +#endif + #define streq(a, b) (strcmp(a, b) == 0) #define strneq(a, b, n) (strncmp(a, b, n) == 0) @@ -118,6 +128,14 @@ struct xf86libinput_tablet_tool { struct libinput_tablet_tool *tool; }; +#if HAVE_LIBINPUT_CUSTOM_ACCEL +struct accel_points { + double step; + double points[CUSTOM_ACCEL_NPOINTS_MAX]; + size_t npoints; +}; +#endif + struct xf86libinput { InputInfoPtr pInfo; char *path; @@ -160,7 +178,10 @@ struct xf86libinput { enum libinput_config_scroll_method scroll_method; enum libinput_config_click_method click_method; enum libinput_config_accel_profile accel_profile; - +#if HAVE_LIBINPUT_CUSTOM_ACCEL + struct accel_points accel_points_fallback; + struct accel_points accel_points_motion; +#endif unsigned char btnmap[MAX_BUTTONS + 1]; BOOL horiz_scrolling_enabled; @@ -514,11 +535,57 @@ LibinputApplyConfigNaturalScroll(DeviceIntPtr dev, driver_data->options.natural_scrolling); } +#if HAVE_LIBINPUT_CUSTOM_ACCEL +static bool +LibinputApplyConfigAccelCustom(struct xf86libinput *driver_data, + struct libinput_device *device) +{ + bool success = false; + struct libinput_config_accel *accel; + enum libinput_config_status status; + + accel = libinput_config_accel_create(LIBINPUT_CONFIG_ACCEL_PROFILE_CUSTOM); + if (!accel) + goto out; + + /* If the step is 0, the user has not set a custom function, + thus we don't set the points */ + if (driver_data->options.accel_points_fallback.step > 0 && + driver_data->options.accel_points_fallback.npoints >= 2) { + status = libinput_config_accel_set_points(accel, + LIBINPUT_ACCEL_TYPE_FALLBACK, + driver_data->options.accel_points_fallback.step, + driver_data->options.accel_points_fallback.npoints, + driver_data->options.accel_points_fallback.points); + if (status != LIBINPUT_CONFIG_STATUS_SUCCESS) + goto out; + } + + if (driver_data->options.accel_points_motion.step > 0 && + driver_data->options.accel_points_motion.npoints >= 2) { + status = libinput_config_accel_set_points(accel, + LIBINPUT_ACCEL_TYPE_MOTION, + driver_data->options.accel_points_motion.step, + driver_data->options.accel_points_motion.npoints, + driver_data->options.accel_points_motion.points); + if (status != LIBINPUT_CONFIG_STATUS_SUCCESS) + goto out; + } + + status = libinput_device_config_accel_apply(device, accel); + success = status == LIBINPUT_CONFIG_STATUS_SUCCESS; +out: + libinput_config_accel_destroy(accel); + return success; +} +#endif + static void LibinputApplyConfigAccel(DeviceIntPtr dev, struct xf86libinput *driver_data, struct libinput_device *device) { + bool success = false; InputInfoPtr pInfo = dev->public.devicePrivate; if (!subdevice_has_capabilities(dev, CAP_POINTER)) @@ -527,15 +594,31 @@ LibinputApplyConfigAccel(DeviceIntPtr dev, if (libinput_device_config_accel_is_available(device) && libinput_device_config_accel_set_speed(device, driver_data->options.speed) != LIBINPUT_CONFIG_STATUS_SUCCESS) - xf86IDrvMsg(pInfo, X_ERROR, - "Failed to set speed %.2f\n", - driver_data->options.speed); - - if (libinput_device_config_accel_get_profiles(device) && - driver_data->options.accel_profile != LIBINPUT_CONFIG_ACCEL_PROFILE_NONE && - libinput_device_config_accel_set_profile(device, - driver_data->options.accel_profile) != - LIBINPUT_CONFIG_STATUS_SUCCESS) { + xf86IDrvMsg(pInfo, X_ERROR, + "Failed to set speed %.2f\n", + driver_data->options.speed); + + if (!libinput_device_config_accel_get_profiles(device) || + driver_data->options.accel_profile == LIBINPUT_CONFIG_ACCEL_PROFILE_NONE) + return; + + switch (driver_data->options.accel_profile) { + case LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT: + case LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE: + success = libinput_device_config_accel_set_profile(device, driver_data->options.accel_profile) == + LIBINPUT_CONFIG_STATUS_SUCCESS; + break; +#if HAVE_LIBINPUT_CUSTOM_ACCEL + case LIBINPUT_CONFIG_ACCEL_PROFILE_CUSTOM: + success = LibinputApplyConfigAccelCustom(driver_data, device); + break; +#endif + default: + success = false; + break; + } + + if (!success) { const char *profile; switch (driver_data->options.accel_profile) { @@ -545,6 +628,11 @@ LibinputApplyConfigAccel(DeviceIntPtr dev, case LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT: profile = "flat"; break; +#if HAVE_LIBINPUT_CUSTOM_ACCEL + case LIBINPUT_CONFIG_ACCEL_PROFILE_CUSTOM: + profile = "custom"; + break; +#endif default: profile = "unknown"; break; @@ -2815,10 +2903,14 @@ xf86libinput_parse_accel_profile_option(InputInfoPtr pInfo, str = xf86SetStrOption(pInfo->options, "AccelProfile", NULL); if (!str) profile = libinput_device_config_accel_get_profile(device); - else if (strncasecmp(str, "adaptive", 9) == 0) + else if (strcasecmp(str, "adaptive") == 0) profile = LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE; - else if (strncasecmp(str, "flat", 4) == 0) + else if (strcasecmp(str, "flat") == 0) profile = LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT; +#if HAVE_LIBINPUT_CUSTOM_ACCEL + else if (strcasecmp(str, "custom") == 0) + profile = LIBINPUT_CONFIG_ACCEL_PROFILE_CUSTOM; +#endif else { xf86IDrvMsg(pInfo, X_ERROR, "Unknown accel profile '%s'. Using default.\n", @@ -2831,6 +2923,106 @@ xf86libinput_parse_accel_profile_option(InputInfoPtr pInfo, return profile; } +#if HAVE_LIBINPUT_CUSTOM_ACCEL +static inline struct accel_points +xf86libinput_parse_accel_points_option(InputInfoPtr pInfo, struct libinput_device *device, const char *name) +{ + struct accel_points accel_points = {0}; + char *str = NULL; + double *points = NULL; + size_t npoints = 0; + + if ((libinput_device_config_accel_get_profiles(device) & LIBINPUT_CONFIG_ACCEL_PROFILE_CUSTOM) == 0) + goto out; + + str = xf86SetStrOption(pInfo->options, name, NULL); + if (!str) + goto out; + + points = double_array_from_string(str, " ", &npoints); + if (!points) { + xf86IDrvMsg(pInfo, X_ERROR, "Failed to parse AccelPoints, ignoring points.\n"); + goto out; + } + + if (npoints < CUSTOM_ACCEL_NPOINTS_MIN) { + xf86IDrvMsg(pInfo, X_ERROR, "At least %d AccelPoints are required, ignoring points.\n", + CUSTOM_ACCEL_NPOINTS_MIN); + goto out; + } + + if (npoints > CUSTOM_ACCEL_NPOINTS_MAX) { + xf86IDrvMsg(pInfo, X_WARNING, "Excessive number of AccelPoints, clipping to first %d points.\n", + CUSTOM_ACCEL_NPOINTS_MAX); + npoints = CUSTOM_ACCEL_NPOINTS_MAX; + } + + for (size_t idx = 0; idx < npoints; idx++) { + if (points[idx] < CUSTOM_ACCEL_POINT_MIN || points[idx] > CUSTOM_ACCEL_POINT_MAX) { + xf86IDrvMsg(pInfo, X_ERROR, "AccelPoints are not in the allowed range between %d and %d, ignoring points.\n", + CUSTOM_ACCEL_POINT_MIN, + CUSTOM_ACCEL_POINT_MAX); + goto out; + } + } + + memcpy(accel_points.points, points, npoints * sizeof(*points)); + accel_points.npoints = npoints; + +out: + free(str); + free(points); + return accel_points; +} + +static inline struct accel_points +xf86libinput_parse_accel_points_fallback_option(InputInfoPtr pInfo, struct libinput_device *device) +{ + return xf86libinput_parse_accel_points_option(pInfo, device, "AccelPointsFallback"); +} + +static inline struct accel_points +xf86libinput_parse_accel_points_motion_option(InputInfoPtr pInfo, struct libinput_device *device) +{ + return xf86libinput_parse_accel_points_option(pInfo, device, "AccelPointsMotion"); +} + +static inline double +xf86libinput_parse_accel_step_option(InputInfoPtr pInfo, struct libinput_device *device, const char *name) +{ + double step = 0.0; + double parsed_step; + + if ((libinput_device_config_accel_get_profiles(device) & LIBINPUT_CONFIG_ACCEL_PROFILE_CUSTOM) == 0) + return step; + + parsed_step = xf86SetRealOption(pInfo->options, name, 0.0); + + if (parsed_step < CUSTOM_ACCEL_STEP_MIN || parsed_step > CUSTOM_ACCEL_STEP_MAX) { + xf86IDrvMsg(pInfo, X_ERROR, "Invalid step value, ignoring step.\n"); + return step; + } + + if (parsed_step == 0) + xf86IDrvMsg(pInfo, X_INFO, "Step value 0 was provided, libinput Fallback acceleration function is used.\n"); + + step = parsed_step; + return step; +} + +static inline double +xf86libinput_parse_accel_step_fallback_option(InputInfoPtr pInfo, struct libinput_device *device) +{ + return xf86libinput_parse_accel_step_option(pInfo, device, "AccelStepFallback"); +} + +static inline double +xf86libinput_parse_accel_step_motion_option(InputInfoPtr pInfo, struct libinput_device *device) +{ + return xf86libinput_parse_accel_step_option(pInfo, device, "AccelStepMotion"); +} +#endif + static inline BOOL xf86libinput_parse_natscroll_option(InputInfoPtr pInfo, struct libinput_device *device) @@ -3365,6 +3557,12 @@ xf86libinput_parse_options(InputInfoPtr pInfo, options->tap_button_map = xf86libinput_parse_tap_buttonmap_option(pInfo, device); options->speed = xf86libinput_parse_accel_option(pInfo, device); options->accel_profile = xf86libinput_parse_accel_profile_option(pInfo, device); +#if HAVE_LIBINPUT_CUSTOM_ACCEL + options->accel_points_fallback = xf86libinput_parse_accel_points_fallback_option(pInfo, device); + options->accel_points_motion = xf86libinput_parse_accel_points_motion_option(pInfo, device); + options->accel_points_fallback.step = xf86libinput_parse_accel_step_fallback_option(pInfo, device); + options->accel_points_motion.step = xf86libinput_parse_accel_step_motion_option(pInfo, device); +#endif options->natural_scrolling = xf86libinput_parse_natscroll_option(pInfo, device); options->sendevents = xf86libinput_parse_sendevents_option(pInfo, device); options->left_handed = xf86libinput_parse_lefthanded_option(pInfo, device); @@ -3825,6 +4023,12 @@ static Atom prop_calibration_default; static Atom prop_accel; static Atom prop_accel_default; static Atom prop_accel_profile_enabled; +#if HAVE_LIBINPUT_CUSTOM_ACCEL +static Atom prop_accel_points_motion; +static Atom prop_accel_points_fallback; +static Atom prop_accel_step_motion; +static Atom prop_accel_step_fallback; +#endif static Atom prop_accel_profile_default; static Atom prop_accel_profiles_available; static Atom prop_natural_scroll; @@ -4194,7 +4398,7 @@ LibinputSetPropertyAccelProfile(DeviceIntPtr dev, BOOL* data; uint32_t profiles = 0; - if (val->format != 8 || val->size != 2 || val->type != XA_INTEGER) + if (val->format != 8 || val->size < 2 || val->size > 3 || val->type != XA_INTEGER) return BadMatch; data = (BOOL*)val->data; @@ -4203,6 +4407,10 @@ LibinputSetPropertyAccelProfile(DeviceIntPtr dev, profiles |= LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE; if (data[1]) profiles |= LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT; +#if HAVE_LIBINPUT_CUSTOM_ACCEL + if (val->size > 2 && data[2]) + profiles |= LIBINPUT_CONFIG_ACCEL_PROFILE_CUSTOM; +#endif if (checkonly) { uint32_t supported; @@ -4223,6 +4431,92 @@ LibinputSetPropertyAccelProfile(DeviceIntPtr dev, return Success; } +#if HAVE_LIBINPUT_CUSTOM_ACCEL +static inline int +LibinputSetPropertyAccelPoints(DeviceIntPtr dev, + Atom atom, + XIPropertyValuePtr val, + BOOL checkonly) +{ + InputInfoPtr pInfo = dev->public.devicePrivate; + struct xf86libinput *driver_data = pInfo->private; + struct libinput_device *device = driver_data->shared_device->device; + float* data; + struct accel_points *accel_points = NULL; + + if (val->format != 32 || val->type != prop_float || + val->size < CUSTOM_ACCEL_NPOINTS_MIN || val->size > CUSTOM_ACCEL_NPOINTS_MAX) + return BadMatch; + + data = (float*)val->data; + + if (checkonly) { + uint32_t profiles; + + if (!xf86libinput_check_device(dev, atom)) + return BadMatch; + + profiles = libinput_device_config_accel_get_profiles(device); + if ((profiles & LIBINPUT_CONFIG_ACCEL_PROFILE_CUSTOM) == 0) + return BadValue; + + for (size_t idx = 0; idx < val->size; idx++) { + if (data[idx] < CUSTOM_ACCEL_POINT_MIN || data[idx] > CUSTOM_ACCEL_POINT_MAX) + return BadValue; + } + } else { + if (atom == prop_accel_points_fallback) + accel_points = &driver_data->options.accel_points_fallback; + else if (atom == prop_accel_points_motion) + accel_points = &driver_data->options.accel_points_motion; + + for (size_t idx = 0; idx < val->size; idx++) + accel_points->points[idx] = data[idx]; + accel_points->npoints = val->size; + } + + return Success; +} + +static inline int +LibinputSetPropertyAccelStep(DeviceIntPtr dev, + Atom atom, + XIPropertyValuePtr val, + BOOL checkonly) +{ + InputInfoPtr pInfo = dev->public.devicePrivate; + struct xf86libinput *driver_data = pInfo->private; + struct libinput_device *device = driver_data->shared_device->device; + float* data; + + if (val->format != 32 || val->type != prop_float || val->size != 1) + return BadMatch; + + data = (float*)val->data; + + if (checkonly) { + uint32_t profiles; + + if (!xf86libinput_check_device(dev, atom)) + return BadMatch; + + profiles = libinput_device_config_accel_get_profiles(device); + if ((profiles & LIBINPUT_CONFIG_ACCEL_PROFILE_CUSTOM) == 0) + return BadValue; + + if (*data < CUSTOM_ACCEL_STEP_MIN || *data > CUSTOM_ACCEL_STEP_MAX) + return BadValue; + } else { + if (atom == prop_accel_step_fallback) + driver_data->options.accel_points_fallback.step = *data; + else if (atom == prop_accel_step_motion) + driver_data->options.accel_points_motion.step = *data; + } + + return Success; +} +#endif + static inline int LibinputSetPropertyNaturalScroll(DeviceIntPtr dev, Atom atom, @@ -4871,6 +5165,12 @@ LibinputSetProperty(DeviceIntPtr dev, Atom atom, XIPropertyValuePtr val, rc = LibinputSetPropertyAccel(dev, atom, val, checkonly); else if (atom == prop_accel_profile_enabled) rc = LibinputSetPropertyAccelProfile(dev, atom, val, checkonly); +#if HAVE_LIBINPUT_CUSTOM_ACCEL + else if (atom == prop_accel_points_fallback || atom == prop_accel_points_motion) + rc = LibinputSetPropertyAccelPoints(dev, atom, val, checkonly); + else if (atom == prop_accel_step_fallback || atom == prop_accel_step_motion) + rc = LibinputSetPropertyAccelStep(dev, atom, val, checkonly); +#endif else if (atom == prop_natural_scroll) rc = LibinputSetPropertyNaturalScroll(dev, atom, val, checkonly); else if (atom == prop_sendevents_enabled) @@ -5152,7 +5452,20 @@ LibinputInitAccelProperty(DeviceIntPtr dev, float speed = driver_data->options.speed; uint32_t profile_mask; enum libinput_config_accel_profile profile; - BOOL profiles[2] = {FALSE}; + BOOL profiles[3] = {FALSE}; +#if HAVE_LIBINPUT_CUSTOM_ACCEL + float custom_points_fallback[CUSTOM_ACCEL_NPOINTS_MAX] = {0}; + float custom_points_motion[CUSTOM_ACCEL_NPOINTS_MAX] = {0}; + size_t custom_npoints_fallback = driver_data->options.accel_points_fallback.npoints; + size_t custom_npoints_motion = driver_data->options.accel_points_motion.npoints; + float custom_step_fallback = driver_data->options.accel_points_fallback.step; + float custom_step_motion = driver_data->options.accel_points_motion.step; + + for (size_t idx = 0; idx < CUSTOM_ACCEL_NPOINTS_MAX; idx++) { + custom_points_fallback[idx] = driver_data->options.accel_points_fallback.points[idx]; + custom_points_motion[idx] = driver_data->options.accel_points_motion.points[idx]; + } +#endif if (!subdevice_has_capabilities(dev, CAP_POINTER)) return; @@ -5182,6 +5495,10 @@ LibinputInitAccelProperty(DeviceIntPtr dev, profiles[0] = TRUE; if (profile_mask & LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT) profiles[1] = TRUE; +#if HAVE_LIBINPUT_CUSTOM_ACCEL + if (profile_mask & LIBINPUT_CONFIG_ACCEL_PROFILE_CUSTOM) + profiles[2] = TRUE; +#endif prop_accel_profiles_available = LibinputMakeProperty(dev, LIBINPUT_PROP_ACCEL_PROFILES_AVAILABLE, @@ -5201,6 +5518,11 @@ LibinputInitAccelProperty(DeviceIntPtr dev, case LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT: profiles[1] = TRUE; break; +#if HAVE_LIBINPUT_CUSTOM_ACCEL + case LIBINPUT_CONFIG_ACCEL_PROFILE_CUSTOM: + profiles[2] = TRUE; + break; +#endif default: break; } @@ -5223,6 +5545,11 @@ LibinputInitAccelProperty(DeviceIntPtr dev, case LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT: profiles[1] = TRUE; break; +#if HAVE_LIBINPUT_CUSTOM_ACCEL + case LIBINPUT_CONFIG_ACCEL_PROFILE_CUSTOM: + profiles[2] = TRUE; + break; +#endif default: break; } @@ -5235,6 +5562,26 @@ LibinputInitAccelProperty(DeviceIntPtr dev, if (!prop_accel_profile_default) return; +#if HAVE_LIBINPUT_CUSTOM_ACCEL + prop_accel_points_fallback = LibinputMakeProperty(dev, + LIBINPUT_PROP_ACCEL_CUSTOM_POINTS_FALLBACK, + prop_float, 32, + custom_npoints_fallback, + custom_points_fallback); + prop_accel_points_motion = LibinputMakeProperty(dev, + LIBINPUT_PROP_ACCEL_CUSTOM_POINTS_MOTION, + prop_float, 32, + custom_npoints_motion, + custom_points_motion); + prop_accel_step_fallback = LibinputMakeProperty(dev, + LIBINPUT_PROP_ACCEL_CUSTOM_STEP_FALLBACK, + prop_float, 32, 1, + &custom_step_fallback); + prop_accel_step_motion = LibinputMakeProperty(dev, + LIBINPUT_PROP_ACCEL_CUSTOM_STEP_MOTION, + prop_float, 32, 1, + &custom_step_motion); +#endif } static void -- cgit v1.2.1