// SPDX-License-Identifier: MIT // Copyright (c) 2011-2013, Christopher Jeffrey // Copyright (c) 2013 Richard Grenville #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // for xcb_render_fixed_t, XXX #include #include "c2.h" #include "common.h" #include "compiler.h" #include "kernel.h" #include "log.h" #include "region.h" #include "string_utils.h" #include "types.h" #include "utils.h" #include "win.h" #include "config.h" struct debug_options global_debug_options; const char *xdg_config_home(void) { char *xdgh = getenv("XDG_CONFIG_HOME"); char *home = getenv("HOME"); const char *default_dir = "/.config"; if (!xdgh) { if (!home) { return NULL; } xdgh = mstrjoin(home, default_dir); } else { xdgh = strdup(xdgh); } return xdgh; } char **xdg_config_dirs(void) { char *xdgd = getenv("XDG_CONFIG_DIRS"); size_t count = 0; if (!xdgd) { xdgd = "/etc/xdg"; } for (int i = 0; xdgd[i]; i++) { if (xdgd[i] == ':') { count++; } } // Store the string and the result pointers together so they can be // freed together char **dir_list = cvalloc(sizeof(char *) * (count + 2) + strlen(xdgd) + 1); auto dirs = strcpy((char *)dir_list + sizeof(char *) * (count + 2), xdgd); auto path = dirs; for (size_t i = 0; i < count; i++) { dir_list[i] = path; path = strchr(path, ':'); *path = '\0'; path++; } dir_list[count] = path; size_t fill = 0; for (size_t i = 0; i <= count; i++) { if (dir_list[i][0] == '/') { dir_list[fill] = dir_list[i]; fill++; } } dir_list[fill] = NULL; return dir_list; } TEST_CASE(xdg_config_dirs) { auto old_var = getenv("XDG_CONFIG_DIRS"); if (old_var) { old_var = strdup(old_var); } unsetenv("XDG_CONFIG_DIRS"); auto result = xdg_config_dirs(); TEST_STREQUAL(result[0], "/etc/xdg"); TEST_EQUAL(result[1], NULL); free(result); setenv("XDG_CONFIG_DIRS", ".:.:/etc/xdg:.:/:", 1); result = xdg_config_dirs(); TEST_STREQUAL(result[0], "/etc/xdg"); TEST_STREQUAL(result[1], "/"); TEST_EQUAL(result[2], NULL); free(result); setenv("XDG_CONFIG_DIRS", ":", 1); result = xdg_config_dirs(); TEST_EQUAL(result[0], NULL); free(result); if (old_var) { setenv("XDG_CONFIG_DIRS", old_var, 1); free(old_var); } } /** * Parse a long number. */ bool parse_long(const char *s, long *dest) { const char *endptr = NULL; long val = strtol(s, (char **)&endptr, 0); if (!endptr || endptr == s) { log_error("Invalid number: %s", s); return false; } while (isspace((unsigned char)*endptr)) { ++endptr; } if (*endptr) { log_error("Trailing characters: %s", s); return false; } *dest = val; return true; } /** * Parse an int number. */ bool parse_int(const char *s, int *dest) { long val; if (!parse_long(s, &val)) { return false; } if (val > INT_MAX || val < INT_MIN) { log_error("Number exceeded int limits: %ld", val); return false; } *dest = (int)val; return true; } /** * Parse a floating-point number in from a string, * also strips the trailing space and comma after the number. * * @param[in] src string to parse * @param[out] dest return the number parsed from the string * @return pointer to the last character parsed */ const char *parse_readnum(const char *src, double *dest) { const char *pc = NULL; double val = strtod_simple(src, &pc); if (!pc || pc == src) { log_error("No number found: %s", src); return src; } while (*pc && (isspace((unsigned char)*pc) || *pc == ',')) { ++pc; } *dest = val; return pc; } int parse_blur_method(const char *src) { if (strcmp(src, "box") == 0) { return BLUR_METHOD_BOX; } if (strcmp(src, "dual_kawase") == 0) { return BLUR_METHOD_DUAL_KAWASE; } if (strcmp(src, "gaussian") == 0) { return BLUR_METHOD_GAUSSIAN; } if (strcmp(src, "kernel") == 0) { return BLUR_METHOD_KERNEL; } if (strcmp(src, "none") == 0) { return BLUR_METHOD_NONE; } return BLUR_METHOD_INVALID; } /** * Parse a matrix. * * @param[in] src the blur kernel string * @param[out] endptr return where the end of kernel is in the string * @param[out] hasneg whether the kernel has negative values */ static conv *parse_blur_kern(const char *src, const char **endptr) { int width = 0, height = 0; const char *pc = NULL; // Get matrix width and height double val = 0.0; if (src == (pc = parse_readnum(src, &val))) { goto err1; } src = pc; width = (int)val; if (src == (pc = parse_readnum(src, &val))) { goto err1; } src = pc; height = (int)val; // Validate matrix width and height if (width <= 0 || height <= 0) { log_error("Blue kernel width/height can't be negative."); goto err1; } if (!(width % 2 && height % 2)) { log_error("Blur kernel width/height must be odd."); goto err1; } if (width > 16 || height > 16) { log_warn("Blur kernel width/height too large, may slow down" "rendering, and/or consume lots of memory"); } // Allocate memory conv *matrix = cvalloc(sizeof(conv) + (size_t)(width * height) * sizeof(double)); // Read elements int skip = height / 2 * width + width / 2; for (int i = 0; i < width * height; ++i) { // Ignore the center element if (i == skip) { matrix->data[i] = 1; continue; } if (src == (pc = parse_readnum(src, &val))) { goto err2; } src = pc; matrix->data[i] = val; } // Detect trailing characters for (; *pc && *pc != ';'; pc++) { if (!isspace((unsigned char)*pc) && *pc != ',') { // TODO(yshui) isspace is locale aware, be careful log_error("Trailing characters in blur kernel string."); goto err2; } } // Jump over spaces after ';' if (*pc == ';') { pc++; while (*pc && isspace((unsigned char)*pc)) { ++pc; } } // Require an end of string if endptr is not provided, otherwise // copy end pointer to endptr if (endptr) { *endptr = pc; } else if (*pc) { log_error("Only one blur kernel expected."); goto err2; } // Fill in width and height matrix->w = width; matrix->h = height; return matrix; err2: free(matrix); err1: return NULL; } /** * Parse a list of convolution kernels. * * @param[in] src string to parse * @param[out] hasneg whether any of the kernels have negative values * @return the kernels */ struct conv **parse_blur_kern_lst(const char *src, int *count) { // TODO(yshui) just return a predefined kernels, not parse predefined strings... static const struct { const char *name; const char *kern_str; } CONV_KERN_PREDEF[] = { {"3x3box", "3,3,1,1,1,1,1,1,1,1,"}, {"5x5box", "5,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,"}, {"7x7box", "7,7,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1," "1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,"}, {"3x3gaussian", "3,3,0.243117,0.493069,0.243117,0.493069,0.493069,0.243117,0." "493069,0.243117,"}, {"5x5gaussian", "5,5,0.003493,0.029143,0.059106,0.029143,0.003493,0.029143,0." "243117,0.493069,0.243117,0.029143,0.059106,0.493069,0." "493069,0.059106,0.029143,0.243117,0.493069,0.243117,0." "029143,0.003493,0.029143,0.059106,0.029143,0.003493,"}, {"7x7gaussian", "7,7,0.000003,0.000102,0.000849,0.001723,0.000849,0.000102,0." "000003,0.000102,0.003493,0.029143,0.059106,0.029143,0." "003493,0.000102,0.000849,0.029143,0.243117,0.493069,0." "243117,0.029143,0.000849,0.001723,0.059106,0.493069,0." "493069,0.059106,0.001723,0.000849,0.029143,0.243117,0." "493069,0.243117,0.029143,0.000849,0.000102,0.003493,0." "029143,0.059106,0.029143,0.003493,0.000102,0.000003,0." "000102,0.000849,0.001723,0.000849,0.000102,0.000003,"}, {"9x9gaussian", "9,9,0.000000,0.000000,0.000001,0.000006,0.000012,0.000006,0.000001,0." "000000,0.000000,0.000000,0.000003,0.000102,0.000849,0.001723,0.000849,0." "000102,0.000003,0.000000,0.000001,0.000102,0.003493,0.029143,0.059106,0." "029143,0.003493,0.000102,0.000001,0.000006,0.000849,0.029143,0.243117,0." "493069,0.243117,0.029143,0.000849,0.000006,0.000012,0.001723,0.059106,0." "493069,0.493069,0.059106,0.001723,0.000012,0.000006,0.000849,0.029143,0." "243117,0.493069,0.243117,0.029143,0.000849,0.000006,0.000001,0.000102,0." "003493,0.029143,0.059106,0.029143,0.003493,0.000102,0.000001,0.000000,0." "000003,0.000102,0.000849,0.001723,0.000849,0.000102,0.000003,0.000000,0." "000000,0.000000,0.000001,0.000006,0.000012,0.000006,0.000001,0.000000,0." "000000,"}, {"11x11gaussian", "11,11,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0." "000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000001,0." "000006,0.000012,0.000006,0.000001,0.000000,0.000000,0.000000,0.000000,0." "000000,0.000003,0.000102,0.000849,0.001723,0.000849,0.000102,0.000003,0." "000000,0.000000,0.000000,0.000001,0.000102,0.003493,0.029143,0.059106,0." "029143,0.003493,0.000102,0.000001,0.000000,0.000000,0.000006,0.000849,0." "029143,0.243117,0.493069,0.243117,0.029143,0.000849,0.000006,0.000000,0." "000000,0.000012,0.001723,0.059106,0.493069,0.493069,0.059106,0.001723,0." "000012,0.000000,0.000000,0.000006,0.000849,0.029143,0.243117,0.493069,0." "243117,0.029143,0.000849,0.000006,0.000000,0.000000,0.000001,0.000102,0." "003493,0.029143,0.059106,0.029143,0.003493,0.000102,0.000001,0.000000,0." "000000,0.000000,0.000003,0.000102,0.000849,0.001723,0.000849,0.000102,0." "000003,0.000000,0.000000,0.000000,0.000000,0.000000,0.000001,0.000006,0." "000012,0.000006,0.000001,0.000000,0.000000,0.000000,0.000000,0.000000,0." "000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0." "000000,"}, }; *count = 0; for (unsigned int i = 0; i < sizeof(CONV_KERN_PREDEF) / sizeof(CONV_KERN_PREDEF[0]); ++i) { if (!strcmp(CONV_KERN_PREDEF[i].name, src)) { return parse_blur_kern_lst(CONV_KERN_PREDEF[i].kern_str, count); } } int nkernels = 1; for (int i = 0; src[i]; i++) { if (src[i] == ';') { nkernels++; } } struct conv **ret = ccalloc(nkernels, struct conv *); int i = 0; const char *pc = src; // Continue parsing until the end of source string i = 0; while (pc && *pc) { assert(i < nkernels); ret[i] = parse_blur_kern(pc, &pc); if (!ret[i]) { for (int j = 0; j < i; j++) { free(ret[j]); } free(ret); return NULL; } i++; } if (i > 1) { log_warn("You are seeing this message because you are using " "multipass blur. Please report an issue to us so we know " "multipass blur is actually been used. Otherwise it might be " "removed in future releases"); } *count = i; return ret; } void *parse_numeric_prefix(const char *src, const char **end, void *user_data) { int *minmax = user_data; *end = NULL; if (!src) { return NULL; } // Find numeric value char *endptr = NULL; long val = strtol(src, &endptr, 0); if (!endptr || endptr == src) { log_error("No number specified: %s", src); return NULL; } if (val < minmax[0] || val > minmax[1]) { log_error("Number not in range (%d <= n <= %d): %s", minmax[0], minmax[1], src); return NULL; } // Skip over spaces while (*endptr && isspace((unsigned char)*endptr)) { ++endptr; } if (':' != *endptr) { log_error("Number separator (':') not found: %s", src); return NULL; } ++endptr; *end = endptr; return (void *)val; } /// Search for auxiliary file under a base directory static char *locate_auxiliary_file_at(const char *base, const char *scope, const char *file) { scoped_charp path = mstrjoin(base, scope); mstrextend(&path, "/"); mstrextend(&path, file); if (access(path, O_RDONLY) == 0) { // Canonicalize path to avoid duplicates char *abspath = realpath(path, NULL); return abspath; } return NULL; } /** * Get a path of an auxiliary file to read, could be a shader file, or any supplementary * file. * * Follows the XDG specification to search for the shader file in configuration locations. * * The search order is: * 1) If an absolute path is given, use it directly. * 2) Search for the file directly under `include_dir`. * 3) Search for the file in the XDG configuration directories, under path * /picom// */ char *locate_auxiliary_file(const char *scope, const char *path, const char *include_dir) { if (!path || strlen(path) == 0) { return NULL; } // Filename is absolute path, so try to load from there if (path[0] == '/') { if (access(path, O_RDONLY) == 0) { return realpath(path, NULL); } } // First try to load file from the include directory (i.e. relative to the // config file) if (include_dir && strlen(include_dir)) { char *ret = locate_auxiliary_file_at(include_dir, "", path); if (ret) { return ret; } } // Fall back to searching in user config directory scoped_charp picom_scope = mstrjoin("/picom/", scope); scoped_charp config_home = (char *)xdg_config_home(); if (config_home) { char *ret = locate_auxiliary_file_at(config_home, picom_scope, path); if (ret) { return ret; } } // Fall back to searching in system config directory auto config_dirs = xdg_config_dirs(); for (int i = 0; config_dirs[i]; i++) { char *ret = locate_auxiliary_file_at(config_dirs[i], picom_scope, path); if (ret) { free(config_dirs); return ret; } } free(config_dirs); return NULL; } struct debug_options_entry { const char *name; const char **choices; size_t offset; }; // clang-format off const char *vblank_scheduler_str[] = { [VBLANK_SCHEDULER_PRESENT] = "present", [VBLANK_SCHEDULER_SGI_VIDEO_SYNC] = "sgi_video_sync", [LAST_VBLANK_SCHEDULER] = NULL }; static const struct debug_options_entry debug_options_entries[] = { {"always_rebind_pixmap", NULL , offsetof(struct debug_options, always_rebind_pixmap)}, {"smart_frame_pacing" , NULL , offsetof(struct debug_options, smart_frame_pacing)}, {"force_vblank_sched" , vblank_scheduler_str, offsetof(struct debug_options, force_vblank_scheduler)}, }; // clang-format on void parse_debug_option_single(char *setting, struct debug_options *debug_options) { char *equal = strchr(setting, '='); size_t name_len = equal ? (size_t)(equal - setting) : strlen(setting); for (size_t i = 0; i < ARR_SIZE(debug_options_entries); i++) { if (strncmp(setting, debug_options_entries[i].name, name_len) != 0) { continue; } if (debug_options_entries[i].name[name_len] != '\0') { continue; } auto value = (int *)((void *)debug_options + debug_options_entries[i].offset); if (equal) { const char *const arg = equal + 1; if (debug_options_entries[i].choices != NULL) { for (size_t j = 0; debug_options_entries[i].choices[j]; j++) { if (strcmp(arg, debug_options_entries[i].choices[j]) == 0) { *value = (int)j; return; } } } if (!parse_int(arg, value)) { log_error("Invalid value for debug option %s: %s, it " "will be ignored.", debug_options_entries[i].name, arg); } } else if (debug_options_entries[i].choices == NULL) { *value = 1; } else { log_error( "Missing value for debug option %s, it will be ignored.", setting); } return; } log_error("Invalid debug option: %s", setting); } /// Parse debug options from environment variable `PICOM_DEBUG`. void parse_debug_options(struct debug_options *debug_options) { const char *debug = getenv("PICOM_DEBUG"); const struct debug_options default_debug_options = { .force_vblank_scheduler = LAST_VBLANK_SCHEDULER, }; *debug_options = default_debug_options; if (!debug) { return; } scoped_charp debug_copy = strdup(debug); char *tmp, *needle = strtok_r(debug_copy, ";", &tmp); while (needle) { parse_debug_option_single(needle, debug_options); needle = strtok_r(NULL, ";", &tmp); } } void *parse_window_shader_prefix(const char *src, const char **end, void *user_data) { const char *include_dir = user_data; *end = NULL; if (!src) { return NULL; } // Find custom shader terminator const char *endptr = strchr(src, ':'); if (!endptr) { log_error("Custom shader terminator not found: %s", src); return NULL; } // Parse and create custom shader scoped_charp untrimed_shader_source = strdup(src); if (!untrimed_shader_source) { return NULL; } auto source_end = strchr(untrimed_shader_source, ':'); *source_end = '\0'; size_t length; char *tmp = (char *)trim_both(untrimed_shader_source, &length); tmp[length] = '\0'; char *shader_source = NULL; if (strcasecmp(tmp, "default") != 0) { shader_source = locate_auxiliary_file("shaders", tmp, include_dir); if (!shader_source) { log_error("Custom shader file \"%s\" not found for rule: %s", tmp, src); free(shader_source); return NULL; } } *end = endptr + 1; return shader_source; } void *parse_window_shader_prefix_with_cwd(const char *src, const char **end, void *) { scoped_charp cwd = getcwd(NULL, 0); return parse_window_shader_prefix(src, end, cwd); } char *parse_config(options_t *opt, const char *config_file) { // clang-format off *opt = (struct options){ .backend = BKEND_XRENDER, .legacy_backends = false, .glx_no_stencil = false, .mark_wmwin_focused = false, .mark_ovredir_focused = false, .detect_rounded_corners = false, .resize_damage = 0, .unredir_if_possible = false, .unredir_if_possible_blacklist = NULL, .unredir_if_possible_delay = 0, .redirected_force = UNSET, .stoppaint_force = UNSET, .dbus = false, .benchmark = 0, .benchmark_wid = XCB_NONE, .logpath = NULL, .log_level = LOG_LEVEL_WARN, .use_damage = true, .frame_pacing = true, .shadow_red = 0.0, .shadow_green = 0.0, .shadow_blue = 0.0, .shadow_radius = 18, .shadow_offset_x = -15, .shadow_offset_y = -15, .shadow_opacity = .75, .shadow_blacklist = NULL, .shadow_ignore_shaped = false, .crop_shadow_to_monitor = false, .shadow_clip_list = NULL, .corner_radius = 0, .fade_in_step = 0.028, .fade_out_step = 0.03, .fade_delta = 10, .no_fading_openclose = false, .no_fading_destroyed_argb = false, .fade_blacklist = NULL, .inactive_opacity = 1.0, .inactive_opacity_override = false, .active_opacity = 1.0, .frame_opacity = 1.0, .detect_client_opacity = false, .blur_method = BLUR_METHOD_NONE, .blur_radius = 3, .blur_deviation = 0.84089642, .blur_strength = 5, .blur_background_frame = false, .blur_background_fixed = false, .blur_background_blacklist = NULL, .blur_kerns = NULL, .blur_kernel_count = 0, .window_shader_fg = NULL, .window_shader_fg_rules = NULL, .inactive_dim = 0.0, .inactive_dim_fixed = false, .invert_color_list = NULL, .opacity_rules = NULL, .max_brightness = 1.0, .use_ewmh_active_win = false, .focus_blacklist = NULL, .detect_transient = false, .detect_client_leader = false, .no_ewmh_fullscreen = false, .track_leader = false, .rounded_corners_blacklist = NULL }; // clang-format on char *ret = NULL; ret = parse_config_libconfig(opt, config_file); return ret; }