mirror of
https://github.com/yshui/picom.git
synced 2025-02-17 15:56:21 -05:00
Merge pull request #1309 from yshui/inspect-monitor
Improvement of `picom-inspect`
This commit is contained in:
commit
2b3745b396
14 changed files with 675 additions and 665 deletions
|
@ -5,7 +5,7 @@
|
|||
* Universal window rules (#1284). One option to rule them all! Added new configuration option `rules` to replace all existing rule options, and to provide more flexibility on top of that. See [picom(1)](https://picom.app/#_window_rules) for more details.
|
||||
* `@include` directives in config file now also search in `$XDG_CONFIG_HOME/picom/include` and `$XDG_CONFIG_DIRS/picom/include`, in addition to relative to the config file's parent directory.
|
||||
* Allow `corner-radius-rules` to override `corner-radius = 0`. Previously setting corner radius to 0 globally disables rounded corners. (#1170)
|
||||
* New `picom-inspect` tool, which lets you test out your picom rules. Sample output:
|
||||
* New `picom-inspect` tool, which lets you test out your picom rules. `man picom-inspect(1)` for more details. Sample output:
|
||||
|
||||
```
|
||||
...
|
||||
|
|
|
@ -19,12 +19,16 @@ DESCRIPTION
|
|||
|
||||
OPTIONS
|
||||
-------
|
||||
*picom-inspect* accept the exact same set of options as *picom*. Naturally, most of those options will not be relevant.
|
||||
*picom-inspect* accepts all options that *picom* does. Naturally, most of those options will not be relevant.
|
||||
|
||||
These are some of the options you might find useful (See *picom*(1) for descriptions of what they do):
|
||||
|
||||
*--config*, *--log-level*, *--log-file*, all the options related to rules.
|
||||
|
||||
*picom-inspect* also accepts some extra options: :::
|
||||
|
||||
*--monitor*:: Keep *picom-inspect* running in a loop, and dump information every time something changed about a window.
|
||||
|
||||
NOTES
|
||||
-----
|
||||
*picom-inspect* is prototype right now. If you find any bug, for example, if rules are matched differently compared to *picom*, please submit bug reports to:
|
||||
|
|
61
src/c2.h
61
src/c2.h
|
@ -8,7 +8,9 @@
|
|||
#include <stddef.h>
|
||||
#include <xcb/xproto.h>
|
||||
|
||||
typedef struct _c2_lptr c2_lptr_t;
|
||||
#include "utils/list.h"
|
||||
|
||||
typedef struct c2_condition c2_condition;
|
||||
typedef struct session session_t;
|
||||
struct c2_state;
|
||||
/// Per-window state used for c2 condition matching.
|
||||
|
@ -19,19 +21,20 @@ struct c2_window_state {
|
|||
};
|
||||
struct atom;
|
||||
struct win;
|
||||
struct list_node;
|
||||
|
||||
typedef void (*c2_userdata_free)(void *);
|
||||
c2_lptr_t *c2_parse(c2_lptr_t **pcondlst, const char *pattern, void *data);
|
||||
struct c2_condition *c2_parse(struct list_node *list, const char *pattern, void *data);
|
||||
|
||||
/// Parse a condition that has a prefix. The prefix is parsed by `parse_prefix`. If
|
||||
/// `free_value` is not NULL, it will be called to free the value returned by
|
||||
/// `parse_prefix` when error occurs.
|
||||
c2_lptr_t *
|
||||
c2_parse_with_prefix(c2_lptr_t **pcondlst, const char *pattern,
|
||||
c2_condition *
|
||||
c2_parse_with_prefix(struct list_node *list, const char *pattern,
|
||||
void *(*parse_prefix)(const char *input, const char **end, void *),
|
||||
void (*free_value)(void *), void *user_data);
|
||||
|
||||
c2_lptr_t *c2_free_lptr(c2_lptr_t *lp, c2_userdata_free f);
|
||||
void c2_free_condition(c2_condition *lp, c2_userdata_free f);
|
||||
|
||||
/// Create a new c2_state object. This is used for maintaining the internal state
|
||||
/// used for c2 condition matching. This state object holds a reference to the
|
||||
|
@ -50,30 +53,46 @@ void c2_window_state_update(struct c2_state *state, struct c2_window_state *wind
|
|||
xcb_connection_t *c, xcb_window_t client_win,
|
||||
xcb_window_t frame_win);
|
||||
|
||||
bool c2_match(struct c2_state *state, const struct win *w, const c2_lptr_t *condlst,
|
||||
void **pdata);
|
||||
bool c2_match_one(struct c2_state *state, const struct win *w, const c2_lptr_t *condlst,
|
||||
void **pdata);
|
||||
bool c2_match(struct c2_state *state, const struct win *w,
|
||||
const struct list_node *conditions, void **pdata);
|
||||
bool c2_match_one(const struct c2_state *state, const struct win *w,
|
||||
const c2_condition *condlst, void **pdata);
|
||||
|
||||
bool c2_list_postprocess(struct c2_state *state, xcb_connection_t *c, c2_lptr_t *list);
|
||||
typedef bool (*c2_list_foreach_cb_t)(const c2_lptr_t *cond, void *data);
|
||||
bool c2_list_foreach(const c2_lptr_t *list, c2_list_foreach_cb_t cb, void *data);
|
||||
bool c2_list_postprocess(struct c2_state *state, xcb_connection_t *c, struct list_node *list);
|
||||
/// Return user data stored in a condition.
|
||||
void *c2_list_get_data(const c2_lptr_t *condlist);
|
||||
void *c2_condition_get_data(const c2_condition *condition);
|
||||
/// Set user data stored in a condition. Return the old user data.
|
||||
void *c2_list_set_data(c2_lptr_t *condlist, void *data);
|
||||
/// Convert a c2_lptr_t to string. The returned string is only valid until the
|
||||
void *c2_condition_set_data(c2_condition *condlist, void *data);
|
||||
/// Convert a c2_condition to string. The returned string is only valid until the
|
||||
/// next call to this function, and should not be freed.
|
||||
const char *c2_lptr_to_str(const c2_lptr_t *);
|
||||
void c2_condlist_insert(c2_lptr_t **pcondlst, c2_lptr_t *pnew);
|
||||
const char *c2_condition_to_str(const c2_condition *);
|
||||
c2_condition *c2_condition_list_next(struct list_node *list, c2_condition *condition);
|
||||
c2_condition *c2_condition_list_prev(struct list_node *list, c2_condition *condition);
|
||||
c2_condition *c2_condition_list_entry(struct list_node *list);
|
||||
/// Create a new condition list with a single condition that is always true.
|
||||
c2_lptr_t *c2_new_true(void);
|
||||
c2_condition *c2_new_true(struct list_node *list);
|
||||
|
||||
#define c2_condition_list_foreach(list, i) \
|
||||
for (c2_condition *i = \
|
||||
list_is_empty((list)) ? NULL : c2_condition_list_entry((list)->next); \
|
||||
i; i = c2_condition_list_next(list, i))
|
||||
#define c2_condition_list_foreach_rev(list, i) \
|
||||
for (c2_condition *i = \
|
||||
list_is_empty((list)) ? NULL : c2_condition_list_entry((list)->prev); \
|
||||
i; i = c2_condition_list_prev(list, i))
|
||||
|
||||
#define c2_condition_list_foreach_safe(list, i, n) \
|
||||
for (c2_condition *i = \
|
||||
list_is_empty((list)) ? NULL : c2_condition_list_entry((list)->next), \
|
||||
*n = c2_condition_list_next(list, i); \
|
||||
i; i = n, n = c2_condition_list_next(list, i))
|
||||
|
||||
/**
|
||||
* Destroy a condition list.
|
||||
*/
|
||||
static inline void c2_list_free(c2_lptr_t **pcondlst, c2_userdata_free f) {
|
||||
while ((*pcondlst = c2_free_lptr(*pcondlst, f))) {
|
||||
static inline void c2_list_free(struct list_node *list, c2_userdata_free f) {
|
||||
c2_condition_list_foreach_safe(list, i, ni) {
|
||||
c2_free_condition(i, f);
|
||||
}
|
||||
*pcondlst = NULL;
|
||||
list_init_head(list);
|
||||
}
|
||||
|
|
15
src/config.c
15
src/config.c
|
@ -724,6 +724,21 @@ bool parse_config(options_t *opt, const char *config_file) {
|
|||
// clang-format on
|
||||
|
||||
list_init_head(&opt->included_config_files);
|
||||
list_init_head(&opt->unredir_if_possible_blacklist);
|
||||
list_init_head(&opt->paint_blacklist);
|
||||
list_init_head(&opt->shadow_blacklist);
|
||||
list_init_head(&opt->shadow_clip_list);
|
||||
list_init_head(&opt->fade_blacklist);
|
||||
list_init_head(&opt->blur_background_blacklist);
|
||||
list_init_head(&opt->invert_color_list);
|
||||
list_init_head(&opt->window_shader_fg_rules);
|
||||
list_init_head(&opt->opacity_rules);
|
||||
list_init_head(&opt->rounded_corners_blacklist);
|
||||
list_init_head(&opt->corner_radius_rules);
|
||||
list_init_head(&opt->focus_blacklist);
|
||||
list_init_head(&opt->transparent_clipping_blacklist);
|
||||
list_init_head(&opt->rules);
|
||||
|
||||
opt->all_scripts = dynarr_new(struct script *, 4);
|
||||
return parse_config_libconfig(opt, config_file);
|
||||
}
|
||||
|
|
33
src/config.h
33
src/config.h
|
@ -59,8 +59,6 @@ typedef struct win_option {
|
|||
bool clip_shadow_above;
|
||||
} win_option_t;
|
||||
|
||||
typedef struct _c2_lptr c2_lptr_t;
|
||||
|
||||
enum vblank_scheduler_type {
|
||||
/// X Present extension based vblank events
|
||||
VBLANK_SCHEDULER_PRESENT,
|
||||
|
@ -241,6 +239,9 @@ typedef struct options {
|
|||
bool print_diagnostics;
|
||||
/// Render to a separate window instead of taking over the screen
|
||||
bool debug_mode;
|
||||
/// For picom-inspect only, dump windows in a loop
|
||||
bool inspect_monitor;
|
||||
xcb_window_t inspect_win;
|
||||
// === General ===
|
||||
/// Use the legacy backends?
|
||||
bool use_legacy_backends;
|
||||
|
@ -273,7 +274,7 @@ typedef struct options {
|
|||
bool unredir_if_possible;
|
||||
/// List of conditions of windows to ignore as a full-screen window
|
||||
/// when determining if a window could be unredirected.
|
||||
c2_lptr_t *unredir_if_possible_blacklist;
|
||||
struct list_node unredir_if_possible_blacklist;
|
||||
/// Delay before unredirecting screen, in milliseconds.
|
||||
int unredir_if_possible_delay;
|
||||
/// Forced redirection setting through D-Bus.
|
||||
|
@ -289,7 +290,7 @@ typedef struct options {
|
|||
/// Window to constantly repaint in benchmark mode. 0 for full-screen.
|
||||
xcb_window_t benchmark_wid;
|
||||
/// A list of conditions of windows not to paint.
|
||||
c2_lptr_t *paint_blacklist;
|
||||
struct list_node paint_blacklist;
|
||||
/// Whether to show all X errors.
|
||||
bool show_all_xerrors;
|
||||
/// Whether to avoid acquiring X Selection.
|
||||
|
@ -318,13 +319,13 @@ typedef struct options {
|
|||
int shadow_offset_x, shadow_offset_y;
|
||||
double shadow_opacity;
|
||||
/// Shadow blacklist. A linked list of conditions.
|
||||
c2_lptr_t *shadow_blacklist;
|
||||
struct list_node shadow_blacklist;
|
||||
/// Whether bounding-shaped window should be ignored.
|
||||
bool shadow_ignore_shaped;
|
||||
/// Whether to crop shadow to the very X RandR monitor.
|
||||
bool crop_shadow_to_monitor;
|
||||
/// Don't draw shadow over these windows. A linked list of conditions.
|
||||
c2_lptr_t *shadow_clip_list;
|
||||
struct list_node shadow_clip_list;
|
||||
bool shadow_enable;
|
||||
|
||||
// === Fading ===
|
||||
|
@ -339,7 +340,7 @@ typedef struct options {
|
|||
/// Whether to disable fading on ARGB managed destroyed windows.
|
||||
bool no_fading_destroyed_argb;
|
||||
/// Fading blacklist. A linked list of conditions.
|
||||
c2_lptr_t *fade_blacklist;
|
||||
struct list_node fade_blacklist;
|
||||
bool fading_enable;
|
||||
|
||||
// === Opacity ===
|
||||
|
@ -374,7 +375,7 @@ typedef struct options {
|
|||
/// to window opacity.
|
||||
bool blur_background_fixed;
|
||||
/// Background blur blacklist. A linked list of conditions.
|
||||
c2_lptr_t *blur_background_blacklist;
|
||||
struct list_node blur_background_blacklist;
|
||||
/// Blur convolution kernel.
|
||||
struct conv **blur_kerns;
|
||||
/// Number of convolution kernels
|
||||
|
@ -382,24 +383,24 @@ typedef struct options {
|
|||
/// Custom fragment shader for painting windows
|
||||
char *window_shader_fg;
|
||||
/// Rules to change custom fragment shader for painting windows.
|
||||
c2_lptr_t *window_shader_fg_rules;
|
||||
struct list_node window_shader_fg_rules;
|
||||
/// How much to dim an inactive window. 0.0 - 1.0, 0 to disable.
|
||||
double inactive_dim;
|
||||
/// Whether to use fixed inactive dim opacity, instead of deciding
|
||||
/// based on window opacity.
|
||||
bool inactive_dim_fixed;
|
||||
/// Conditions of windows to have inverted colors.
|
||||
c2_lptr_t *invert_color_list;
|
||||
struct list_node invert_color_list;
|
||||
/// Rules to change window opacity.
|
||||
c2_lptr_t *opacity_rules;
|
||||
struct list_node opacity_rules;
|
||||
/// Limit window brightness
|
||||
double max_brightness;
|
||||
// Radius of rounded window corners
|
||||
int corner_radius;
|
||||
/// Rounded corners blacklist. A linked list of conditions.
|
||||
c2_lptr_t *rounded_corners_blacklist;
|
||||
struct list_node rounded_corners_blacklist;
|
||||
/// Rounded corner rules. A linked list of conditions.
|
||||
c2_lptr_t *corner_radius_rules;
|
||||
struct list_node corner_radius_rules;
|
||||
|
||||
// === Focus related ===
|
||||
/// Whether to try to detect WM windows and mark them as focused.
|
||||
|
@ -409,7 +410,7 @@ typedef struct options {
|
|||
/// Whether to use EWMH _NET_ACTIVE_WINDOW to find active window.
|
||||
bool use_ewmh_active_win;
|
||||
/// A list of windows always to be considered focused.
|
||||
c2_lptr_t *focus_blacklist;
|
||||
struct list_node focus_blacklist;
|
||||
/// Whether to do window grouping with <code>WM_TRANSIENT_FOR</code>.
|
||||
bool detect_transient;
|
||||
/// Whether to do window grouping with <code>WM_CLIENT_LEADER</code>.
|
||||
|
@ -427,7 +428,7 @@ typedef struct options {
|
|||
bool transparent_clipping;
|
||||
/// A list of conditions of windows to which transparent clipping
|
||||
/// should not apply
|
||||
c2_lptr_t *transparent_clipping_blacklist;
|
||||
struct list_node transparent_clipping_blacklist;
|
||||
|
||||
bool dithered_present;
|
||||
// === Animation ===
|
||||
|
@ -435,7 +436,7 @@ typedef struct options {
|
|||
/// Array of all the scripts used in `animations`. This is a dynarr.
|
||||
struct script **all_scripts;
|
||||
|
||||
c2_lptr_t *rules;
|
||||
struct list_node rules;
|
||||
bool has_both_style_of_rules;
|
||||
} options_t;
|
||||
|
||||
|
|
|
@ -124,7 +124,7 @@ FILE *open_config_file(const char *cpath, char **ppath) {
|
|||
/**
|
||||
* Parse a condition list in configuration file.
|
||||
*/
|
||||
bool must_use parse_cfg_condlst(const config_t *pcfg, c2_lptr_t **pcondlst, const char *name) {
|
||||
bool must_use parse_cfg_condlst(struct list_node *list, const config_t *pcfg, const char *name) {
|
||||
config_setting_t *setting = config_lookup(pcfg, name);
|
||||
if (setting == NULL) {
|
||||
return true;
|
||||
|
@ -133,15 +133,14 @@ bool must_use parse_cfg_condlst(const config_t *pcfg, c2_lptr_t **pcondlst, cons
|
|||
if (config_setting_is_array(setting)) {
|
||||
int i = config_setting_length(setting);
|
||||
while (i--) {
|
||||
if (!c2_parse(pcondlst,
|
||||
config_setting_get_string_elem(setting, i), NULL)) {
|
||||
if (!c2_parse(list, config_setting_get_string_elem(setting, i), NULL)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Treat it as a single pattern if it's a string
|
||||
else if (CONFIG_TYPE_STRING == config_setting_type(setting)) {
|
||||
if (!c2_parse(pcondlst, config_setting_get_string(setting), NULL)) {
|
||||
if (!c2_parse(list, config_setting_get_string(setting), NULL)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -152,7 +151,7 @@ bool must_use parse_cfg_condlst(const config_t *pcfg, c2_lptr_t **pcondlst, cons
|
|||
* Parse a window corner radius rule list in configuration file.
|
||||
*/
|
||||
static inline bool
|
||||
parse_cfg_condlst_with_prefix(c2_lptr_t **condlst, const config_t *pcfg, const char *name,
|
||||
parse_cfg_condlst_with_prefix(struct list_node *list, const config_t *pcfg, const char *name,
|
||||
void *(*parse_prefix)(const char *, const char **, void *),
|
||||
void (*free_value)(void *), void *user_data) {
|
||||
config_setting_t *setting = config_lookup(pcfg, name);
|
||||
|
@ -164,7 +163,7 @@ parse_cfg_condlst_with_prefix(c2_lptr_t **condlst, const config_t *pcfg, const c
|
|||
int i = config_setting_length(setting);
|
||||
while (i--) {
|
||||
if (!c2_parse_with_prefix(
|
||||
condlst, config_setting_get_string_elem(setting, i),
|
||||
list, config_setting_get_string_elem(setting, i),
|
||||
parse_prefix, free_value, user_data)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -172,7 +171,7 @@ parse_cfg_condlst_with_prefix(c2_lptr_t **condlst, const config_t *pcfg, const c
|
|||
}
|
||||
// Treat it as a single pattern if it's a string
|
||||
else if (config_setting_type(setting) == CONFIG_TYPE_STRING) {
|
||||
if (!c2_parse_with_prefix(condlst, config_setting_get_string(setting),
|
||||
if (!c2_parse_with_prefix(list, config_setting_get_string(setting),
|
||||
parse_prefix, free_value, user_data)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -576,7 +575,8 @@ static const struct {
|
|||
{"transparent-clipping", offsetof(struct window_maybe_options, transparent_clipping)},
|
||||
};
|
||||
|
||||
static c2_lptr_t *parse_rule(config_setting_t *setting, struct script ***out_scripts) {
|
||||
static c2_condition *
|
||||
parse_rule(struct list_node *rules, config_setting_t *setting, struct script ***out_scripts) {
|
||||
if (!config_setting_is_group(setting)) {
|
||||
log_error("Invalid rule at line %d. It must be a group.",
|
||||
config_setting_source_line(setting));
|
||||
|
@ -585,9 +585,9 @@ static c2_lptr_t *parse_rule(config_setting_t *setting, struct script ***out_scr
|
|||
int ival;
|
||||
double fval;
|
||||
const char *sval;
|
||||
c2_lptr_t *rule = NULL;
|
||||
c2_condition *rule = NULL;
|
||||
if (config_setting_lookup_string(setting, "match", &sval)) {
|
||||
rule = c2_parse(NULL, sval, NULL);
|
||||
rule = c2_parse(rules, sval, NULL);
|
||||
if (!rule) {
|
||||
log_error("Failed to parse rule at line %d.",
|
||||
config_setting_source_line(setting));
|
||||
|
@ -595,12 +595,12 @@ static c2_lptr_t *parse_rule(config_setting_t *setting, struct script ***out_scr
|
|||
}
|
||||
} else {
|
||||
// If no match condition is specified, it matches all windows
|
||||
rule = c2_new_true();
|
||||
rule = c2_new_true(rules);
|
||||
}
|
||||
|
||||
auto wopts = cmalloc(struct window_maybe_options);
|
||||
*wopts = WIN_MAYBE_OPTIONS_DEFAULT;
|
||||
c2_list_set_data(rule, wopts);
|
||||
c2_condition_set_data(rule, wopts);
|
||||
|
||||
for (size_t i = 0; i < ARR_SIZE(all_window_options); i++) {
|
||||
if (config_setting_lookup_bool(setting, all_window_options[i].name, &ival)) {
|
||||
|
@ -630,8 +630,8 @@ static c2_lptr_t *parse_rule(config_setting_t *setting, struct script ***out_scr
|
|||
return rule;
|
||||
}
|
||||
|
||||
static void
|
||||
parse_rules(config_setting_t *setting, struct script ***out_scripts, c2_lptr_t **rules) {
|
||||
static void parse_rules(struct list_node *rules, config_setting_t *setting,
|
||||
struct script ***out_scripts) {
|
||||
if (!config_setting_is_list(setting)) {
|
||||
log_error("Invalid value for \"rules\" at line %d. It must be a list.",
|
||||
config_setting_source_line(setting));
|
||||
|
@ -640,10 +640,7 @@ parse_rules(config_setting_t *setting, struct script ***out_scripts, c2_lptr_t *
|
|||
const auto length = (unsigned int)config_setting_length(setting);
|
||||
for (unsigned int i = 0; i < length; i++) {
|
||||
auto sub = config_setting_get_elem(setting, i);
|
||||
auto rule = parse_rule(sub, out_scripts);
|
||||
if (rule != NULL) {
|
||||
c2_condlist_insert(rules, rule);
|
||||
}
|
||||
parse_rule(rules, sub, out_scripts);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -766,7 +763,7 @@ bool parse_config_libconfig(options_t *opt, const char *config_file) {
|
|||
|
||||
config_setting_t *rules = config_lookup(&cfg, "rules");
|
||||
if (rules) {
|
||||
parse_rules(rules, &opt->all_scripts, &opt->rules);
|
||||
parse_rules(&opt->rules, rules, &opt->all_scripts);
|
||||
}
|
||||
|
||||
// --dbus
|
||||
|
@ -795,7 +792,7 @@ bool parse_config_libconfig(options_t *opt, const char *config_file) {
|
|||
// -i (inactive_opacity)
|
||||
if (config_lookup_float(&cfg, "inactive-opacity", &dval)) {
|
||||
opt->inactive_opacity = normalize_d(dval);
|
||||
if (opt->rules) {
|
||||
if (!list_is_empty(&opt->rules)) {
|
||||
log_warn_both_style_of_rules("inactive-opacity");
|
||||
opt->has_both_style_of_rules = true;
|
||||
}
|
||||
|
@ -803,7 +800,7 @@ bool parse_config_libconfig(options_t *opt, const char *config_file) {
|
|||
// --active_opacity
|
||||
if (config_lookup_float(&cfg, "active-opacity", &dval)) {
|
||||
opt->active_opacity = normalize_d(dval);
|
||||
if (opt->rules) {
|
||||
if (!list_is_empty(&opt->rules)) {
|
||||
log_warn_both_style_of_rules("active-opacity");
|
||||
opt->has_both_style_of_rules = true;
|
||||
}
|
||||
|
@ -859,30 +856,31 @@ bool parse_config_libconfig(options_t *opt, const char *config_file) {
|
|||
}
|
||||
// --inactive-opacity-override
|
||||
if (lcfg_lookup_bool(&cfg, "inactive-opacity-override", &opt->inactive_opacity_override) &&
|
||||
opt->rules != NULL) {
|
||||
!list_is_empty(&opt->rules)) {
|
||||
log_warn_both_style_of_rules("inactive-opacity-override");
|
||||
opt->has_both_style_of_rules = true;
|
||||
}
|
||||
// --inactive-dim
|
||||
if (config_lookup_float(&cfg, "inactive-dim", &opt->inactive_dim) && opt->rules != NULL) {
|
||||
if (config_lookup_float(&cfg, "inactive-dim", &opt->inactive_dim) &&
|
||||
!list_is_empty(&opt->rules)) {
|
||||
log_warn_both_style_of_rules("inactive-dim");
|
||||
opt->has_both_style_of_rules = true;
|
||||
}
|
||||
// --mark-wmwin-focused
|
||||
if (lcfg_lookup_bool(&cfg, "mark-wmwin-focused", &opt->mark_wmwin_focused) &&
|
||||
opt->rules != NULL) {
|
||||
!list_is_empty(&opt->rules)) {
|
||||
log_warn_both_style_of_rules("mark-wmwin-focused");
|
||||
opt->has_both_style_of_rules = true;
|
||||
}
|
||||
// --mark-ovredir-focused
|
||||
if (lcfg_lookup_bool(&cfg, "mark-ovredir-focused", &opt->mark_ovredir_focused) &&
|
||||
opt->rules != NULL) {
|
||||
!list_is_empty(&opt->rules)) {
|
||||
log_warn_both_style_of_rules("mark-ovredir-focused");
|
||||
opt->has_both_style_of_rules = true;
|
||||
}
|
||||
// --shadow-ignore-shaped
|
||||
if (lcfg_lookup_bool(&cfg, "shadow-ignore-shaped", &opt->shadow_ignore_shaped) &&
|
||||
opt->rules != NULL) {
|
||||
!list_is_empty(&opt->rules)) {
|
||||
log_warn_both_style_of_rules("shadow-ignore-shaped");
|
||||
opt->has_both_style_of_rules = true;
|
||||
}
|
||||
|
@ -965,7 +963,7 @@ bool parse_config_libconfig(options_t *opt, const char *config_file) {
|
|||
// --dithered_present
|
||||
lcfg_lookup_bool(&cfg, "dithered-present", &opt->dithered_present);
|
||||
|
||||
if (opt->rules != NULL) {
|
||||
if (!list_is_empty(&opt->rules)) {
|
||||
static const char *rule_list[] = {
|
||||
"transparent-clipping-exclude",
|
||||
"shadow-exclude",
|
||||
|
@ -987,18 +985,18 @@ bool parse_config_libconfig(options_t *opt, const char *config_file) {
|
|||
opt->has_both_style_of_rules = true;
|
||||
}
|
||||
}
|
||||
} else if (!parse_cfg_condlst(&cfg, &opt->transparent_clipping_blacklist,
|
||||
} else if (!parse_cfg_condlst(&opt->transparent_clipping_blacklist, &cfg,
|
||||
"transparent-clipping-exclude") ||
|
||||
!parse_cfg_condlst(&cfg, &opt->shadow_blacklist, "shadow-exclude") ||
|
||||
!parse_cfg_condlst(&cfg, &opt->shadow_clip_list, "clip-shadow-above") ||
|
||||
!parse_cfg_condlst(&cfg, &opt->fade_blacklist, "fade-exclude") ||
|
||||
!parse_cfg_condlst(&cfg, &opt->focus_blacklist, "focus-exclude") ||
|
||||
!parse_cfg_condlst(&cfg, &opt->invert_color_list, "invert-color-include") ||
|
||||
!parse_cfg_condlst(&cfg, &opt->blur_background_blacklist,
|
||||
!parse_cfg_condlst(&opt->shadow_blacklist, &cfg, "shadow-exclude") ||
|
||||
!parse_cfg_condlst(&opt->shadow_clip_list, &cfg, "clip-shadow-above") ||
|
||||
!parse_cfg_condlst(&opt->fade_blacklist, &cfg, "fade-exclude") ||
|
||||
!parse_cfg_condlst(&opt->focus_blacklist, &cfg, "focus-exclude") ||
|
||||
!parse_cfg_condlst(&opt->invert_color_list, &cfg, "invert-color-include") ||
|
||||
!parse_cfg_condlst(&opt->blur_background_blacklist, &cfg,
|
||||
"blur-background-exclude") ||
|
||||
!parse_cfg_condlst(&cfg, &opt->unredir_if_possible_blacklist,
|
||||
!parse_cfg_condlst(&opt->unredir_if_possible_blacklist, &cfg,
|
||||
"unredir-if-possible-exclude") ||
|
||||
!parse_cfg_condlst(&cfg, &opt->rounded_corners_blacklist,
|
||||
!parse_cfg_condlst(&opt->rounded_corners_blacklist, &cfg,
|
||||
"rounded-corners-exclude") ||
|
||||
!parse_cfg_condlst_with_prefix(
|
||||
&opt->corner_radius_rules, &cfg, "corner-radius-rules",
|
||||
|
@ -1139,7 +1137,7 @@ bool parse_config_libconfig(options_t *opt, const char *config_file) {
|
|||
// Wintype settings
|
||||
|
||||
// XXX ! Refactor all the wintype_* arrays into a struct
|
||||
if (opt->rules == NULL) {
|
||||
if (list_is_empty(&opt->rules)) {
|
||||
for (wintype_t i = 0; i < NUM_WINTYPES; ++i) {
|
||||
parse_wintype_config(&cfg, WINTYPES[i].name, &opt->wintype_option[i],
|
||||
&opt->wintype_option_mask[i]);
|
||||
|
|
324
src/inspect.c
324
src/inspect.c
|
@ -11,92 +11,18 @@
|
|||
|
||||
#include "inspect.h"
|
||||
|
||||
#include "atom.h"
|
||||
#include "backend/backend.h"
|
||||
#include "c2.h"
|
||||
#include "common.h"
|
||||
#include "config.h"
|
||||
#include "log.h"
|
||||
#include "options.h"
|
||||
#include "utils/misc.h"
|
||||
#include "utils/console.h"
|
||||
#include "utils/dynarr.h"
|
||||
#include "utils/str.h"
|
||||
#include "wm/defs.h"
|
||||
#include "wm/win.h"
|
||||
#include "x.h"
|
||||
|
||||
static struct win *
|
||||
setup_window(struct x_connection *c, struct atom *atoms, struct options *options,
|
||||
struct wm *wm, struct c2_state *state, xcb_window_t target) {
|
||||
// Pretend we are the compositor, and build up the window state
|
||||
auto cursor = wm_find(wm, target);
|
||||
if (cursor == NULL) {
|
||||
log_fatal("Could not find window %#010x", target);
|
||||
wm_free(wm);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto toplevel = wm_ref_toplevel_of(wm, cursor);
|
||||
BUG_ON_NULL(toplevel);
|
||||
struct win *w = ccalloc(1, struct win);
|
||||
w->state = WSTATE_MAPPED;
|
||||
w->tree_ref = toplevel;
|
||||
log_debug("Toplevel is %#010x", wm_ref_win_id(toplevel));
|
||||
log_debug("Client is %#010x", win_client_id(w, true));
|
||||
win_update_wintype(c, atoms, w);
|
||||
win_update_frame_extents(c, atoms, w, win_client_id(w, /*fallback_to_self=*/true),
|
||||
options->frame_opacity);
|
||||
// TODO(yshui) get leader
|
||||
win_update_name(c, atoms, w);
|
||||
win_update_class(c, atoms, w);
|
||||
win_update_role(c, atoms, w);
|
||||
|
||||
auto geometry_reply = XCB_AWAIT(xcb_get_geometry, c->c, win_id(w));
|
||||
w->g = (struct win_geometry){
|
||||
.x = geometry_reply->x,
|
||||
.y = geometry_reply->y,
|
||||
.width = geometry_reply->width,
|
||||
.height = geometry_reply->height,
|
||||
};
|
||||
free(geometry_reply);
|
||||
|
||||
auto shape_info = xcb_get_extension_data(c->c, &xcb_shape_id);
|
||||
win_on_win_size_change(w, options->shadow_offset_x, options->shadow_offset_y,
|
||||
options->shadow_radius);
|
||||
win_update_bounding_shape(c, w, shape_info->present, options->detect_rounded_corners);
|
||||
win_update_prop_fullscreen(c, atoms, w);
|
||||
|
||||
// Determine if the window is focused
|
||||
xcb_window_t wid = XCB_NONE;
|
||||
bool exists;
|
||||
if (options->use_ewmh_active_win) {
|
||||
wid_get_prop_window(c, c->screen_info->root, atoms->a_NET_ACTIVE_WINDOW,
|
||||
&exists);
|
||||
} else {
|
||||
// Determine the currently focused window so we can apply appropriate
|
||||
// opacity on it
|
||||
xcb_get_input_focus_reply_t *reply =
|
||||
xcb_get_input_focus_reply(c->c, xcb_get_input_focus(c->c), NULL);
|
||||
|
||||
if (reply) {
|
||||
wid = reply->focus;
|
||||
free(reply);
|
||||
}
|
||||
}
|
||||
if (wid == win_id(w) || wid == win_client_id(w, /*fallback_to_self=*/false)) {
|
||||
w->is_focused = true;
|
||||
}
|
||||
|
||||
auto attributes_reply = XCB_AWAIT(xcb_get_window_attributes, c->c, win_id(w));
|
||||
w->a = *attributes_reply;
|
||||
w->pictfmt = x_get_pictform_for_visual(c, w->a.visual);
|
||||
free(attributes_reply);
|
||||
|
||||
c2_window_state_init(state, &w->c2_state);
|
||||
c2_window_state_update(state, &w->c2_state, c->c,
|
||||
win_client_id(w, /*fallback_to_self=*/true), win_id(w));
|
||||
return w;
|
||||
}
|
||||
|
||||
xcb_window_t select_window(struct x_connection *c) {
|
||||
xcb_window_t inspect_select_window(struct x_connection *c) {
|
||||
xcb_font_t font = x_new_id(c);
|
||||
xcb_cursor_t cursor = x_new_id(c);
|
||||
const char font_name[] = "cursor";
|
||||
|
@ -153,119 +79,54 @@ xcb_window_t select_window(struct x_connection *c) {
|
|||
}
|
||||
|
||||
struct c2_match_state {
|
||||
struct c2_state *state;
|
||||
struct win *w;
|
||||
const struct c2_state *state;
|
||||
const struct win *w;
|
||||
bool print_value;
|
||||
};
|
||||
|
||||
bool c2_match_once_and_log(const c2_lptr_t *cond, void *data) {
|
||||
struct c2_match_state *state = data;
|
||||
static bool c2_match_and_log(const struct list_node *list, const struct c2_state *state,
|
||||
const struct win *w, bool print_value) {
|
||||
void *rule_data = NULL;
|
||||
printf(" %s ... ", c2_lptr_to_str(cond));
|
||||
bool matched = c2_match_one(state->state, state->w, cond, rule_data);
|
||||
printf("%s", matched ? "\033[1;32mmatched\033[0m" : "not matched");
|
||||
if (state->print_value && matched) {
|
||||
printf("/%lu", (unsigned long)(intptr_t)rule_data);
|
||||
state->print_value = false;
|
||||
c2_condition_list_foreach((struct list_node *)list, i) {
|
||||
printf(" %s ... ", c2_condition_to_str(i));
|
||||
bool matched = c2_match_one(state, w, i, rule_data);
|
||||
printf("%s", matched ? "\033[1;32mmatched\033[0m" : "not matched");
|
||||
if (print_value && matched) {
|
||||
printf("/%lu", (unsigned long)(intptr_t)rule_data);
|
||||
print_value = false;
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
printf("\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
#define BOLD(str) "\033[1m" str "\033[0m"
|
||||
void inspect_dump_window(const struct c2_state *state, const struct options *opts,
|
||||
const struct win *w) {
|
||||
if (list_is_empty(&opts->rules)) {
|
||||
printf("Checking " BOLD("transparent-clipping-exclude") ":\n");
|
||||
c2_match_and_log(&opts->transparent_clipping_blacklist, state, w, false);
|
||||
printf("Checking " BOLD("shadow-exclude") ":\n");
|
||||
c2_match_and_log(&opts->shadow_blacklist, state, w, false);
|
||||
printf("Checking " BOLD("fade-exclude") ":\n");
|
||||
c2_match_and_log(&opts->fade_blacklist, state, w, false);
|
||||
printf("Checking " BOLD("clip-shadow-above") ":\n");
|
||||
c2_match_and_log(&opts->shadow_clip_list, state, w, true);
|
||||
printf("Checking " BOLD("focus-exclude") ":\n");
|
||||
c2_match_and_log(&opts->focus_blacklist, state, w, false);
|
||||
printf("Checking " BOLD("invert-color-include") ":\n");
|
||||
c2_match_and_log(&opts->invert_color_list, state, w, false);
|
||||
printf("Checking " BOLD("blur-background-exclude") ":\n");
|
||||
c2_match_and_log(&opts->blur_background_blacklist, state, w, false);
|
||||
printf("Checking " BOLD("unredir-if-possible-exclude") ":\n");
|
||||
c2_match_and_log(&opts->unredir_if_possible_blacklist, state, w, false);
|
||||
printf("Checking " BOLD("rounded-corners-exclude") ":\n");
|
||||
c2_match_and_log(&opts->rounded_corners_blacklist, state, w, false);
|
||||
|
||||
int inspect_main(int argc, char **argv, const char *config_file) {
|
||||
Display *dpy = XOpenDisplay(NULL);
|
||||
if (!dpy) {
|
||||
log_fatal("Can't open display");
|
||||
return 1;
|
||||
printf("Checking " BOLD("opacity-rule") ":\n");
|
||||
c2_match_and_log(&opts->opacity_rules, state, w, true);
|
||||
printf("Checking " BOLD("corner-radius-rule") ":\n");
|
||||
c2_match_and_log(&opts->corner_radius_rules, state, w, true);
|
||||
}
|
||||
struct x_connection c;
|
||||
x_connection_init(&c, dpy);
|
||||
|
||||
xcb_prefetch_extension_data(c.c, &xcb_shape_id);
|
||||
|
||||
struct options options;
|
||||
if (!parse_config(&options, config_file)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Parse all of the rest command line options
|
||||
options.backend = backend_find("dummy");
|
||||
if (!get_cfg(&options, argc, argv)) {
|
||||
log_fatal("Failed to get configuration, usually mean you have specified "
|
||||
"invalid options.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto atoms = init_atoms(c.c);
|
||||
auto state = c2_state_new(atoms);
|
||||
options_postprocess_c2_lists(state, &c, &options);
|
||||
|
||||
struct wm *wm = wm_new();
|
||||
|
||||
wm_import_start(wm, &c, atoms, c.screen_info->root, NULL);
|
||||
// Process events until the window tree is consistent
|
||||
while (x_has_pending_requests(&c)) {
|
||||
auto ev = x_poll_for_event(&c);
|
||||
if (ev == NULL) {
|
||||
continue;
|
||||
}
|
||||
switch (ev->response_type) {
|
||||
case XCB_CREATE_NOTIFY:;
|
||||
auto create = (xcb_create_notify_event_t *)ev;
|
||||
auto parent = wm_find(wm, create->parent);
|
||||
wm_import_start(wm, &c, atoms,
|
||||
((xcb_create_notify_event_t *)ev)->window, parent);
|
||||
break;
|
||||
case XCB_DESTROY_NOTIFY:
|
||||
wm_destroy(wm, ((xcb_destroy_notify_event_t *)ev)->window);
|
||||
break;
|
||||
case XCB_REPARENT_NOTIFY:;
|
||||
auto reparent = (xcb_reparent_notify_event_t *)ev;
|
||||
wm_reparent(wm, reparent->window, reparent->parent);
|
||||
break;
|
||||
default:
|
||||
// Ignore ConfigureNotify and CirculateNotify, because we don't
|
||||
// use stacking order for window rules.
|
||||
break;
|
||||
}
|
||||
free(ev);
|
||||
}
|
||||
|
||||
auto target = select_window(&c);
|
||||
log_info("Target window: %#x", target);
|
||||
auto w = setup_window(&c, atoms, &options, wm, state, target);
|
||||
struct c2_match_state match_state = {
|
||||
.state = state,
|
||||
.w = w,
|
||||
};
|
||||
printf("Checking " BOLD("transparent-clipping-exclude") ":\n");
|
||||
c2_list_foreach(options.transparent_clipping_blacklist, c2_match_once_and_log,
|
||||
&match_state);
|
||||
printf("Checking " BOLD("shadow-exclude") ":\n");
|
||||
c2_list_foreach(options.shadow_blacklist, c2_match_once_and_log, &match_state);
|
||||
printf("Checking " BOLD("fade-exclude") ":\n");
|
||||
c2_list_foreach(options.fade_blacklist, c2_match_once_and_log, &match_state);
|
||||
printf("Checking " BOLD("clip-shadow-above") ":\n");
|
||||
c2_list_foreach(options.shadow_clip_list, c2_match_once_and_log, &match_state);
|
||||
printf("Checking " BOLD("focus-exclude") ":\n");
|
||||
c2_list_foreach(options.focus_blacklist, c2_match_once_and_log, &match_state);
|
||||
printf("Checking " BOLD("invert-color-include") ":\n");
|
||||
c2_list_foreach(options.invert_color_list, c2_match_once_and_log, &match_state);
|
||||
printf("Checking " BOLD("blur-background-exclude") ":\n");
|
||||
c2_list_foreach(options.blur_background_blacklist, c2_match_once_and_log, &match_state);
|
||||
printf("Checking " BOLD("unredir-if-possible-exclude") ":\n");
|
||||
c2_list_foreach(options.unredir_if_possible_blacklist, c2_match_once_and_log,
|
||||
&match_state);
|
||||
printf("Checking " BOLD("rounded-corners-exclude") ":\n");
|
||||
c2_list_foreach(options.rounded_corners_blacklist, c2_match_once_and_log, &match_state);
|
||||
|
||||
match_state.print_value = true;
|
||||
printf("Checking " BOLD("opacity-rule") ":\n");
|
||||
c2_list_foreach(options.opacity_rules, c2_match_once_and_log, &match_state);
|
||||
printf("Checking " BOLD("corner-radius-rule") ":\n");
|
||||
c2_list_foreach(options.corner_radius_rules, c2_match_once_and_log, &match_state);
|
||||
|
||||
printf("\nHere are some rule(s) that match this window:\n");
|
||||
if (w->name != NULL) {
|
||||
|
@ -292,21 +153,88 @@ int inspect_main(int argc, char **argv, const char *config_file) {
|
|||
printf(" bounding_shaped\n");
|
||||
}
|
||||
printf(" border_width = %d\n", w->g.border_width);
|
||||
|
||||
pixman_region32_fini(&w->bounding_shape);
|
||||
free(w->name);
|
||||
free(w->class_instance);
|
||||
free(w->class_general);
|
||||
free(w->role);
|
||||
c2_window_state_destroy(state, &w->c2_state);
|
||||
free(w);
|
||||
|
||||
wm_free(wm);
|
||||
|
||||
log_deinit_tls();
|
||||
c2_state_free(state);
|
||||
destroy_atoms(atoms);
|
||||
options_destroy(&options);
|
||||
XCloseDisplay(c.dpy);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void inspect_dump_window_maybe_options(struct window_maybe_options wopts) {
|
||||
bool nothing = true;
|
||||
printf(" Applying:\n");
|
||||
if (wopts.shadow != TRI_UNKNOWN) {
|
||||
printf(" shadow = %s\n", wopts.shadow == TRI_TRUE ? "true" : "false");
|
||||
nothing = false;
|
||||
}
|
||||
if (wopts.fade != TRI_UNKNOWN) {
|
||||
printf(" fade = %s\n", wopts.fade == TRI_TRUE ? "true" : "false");
|
||||
nothing = false;
|
||||
}
|
||||
if (wopts.blur_background != TRI_UNKNOWN) {
|
||||
printf(" blur_background = %s\n",
|
||||
wopts.blur_background == TRI_TRUE ? "true" : "false");
|
||||
nothing = false;
|
||||
}
|
||||
if (wopts.invert_color != TRI_UNKNOWN) {
|
||||
printf(" invert_color = %s\n",
|
||||
wopts.invert_color == TRI_TRUE ? "true" : "false");
|
||||
nothing = false;
|
||||
}
|
||||
if (wopts.clip_shadow_above != TRI_UNKNOWN) {
|
||||
printf(" clip_shadow_above = %s\n",
|
||||
wopts.clip_shadow_above == TRI_TRUE ? "true" : "false");
|
||||
nothing = false;
|
||||
}
|
||||
if (wopts.transparent_clipping != TRI_UNKNOWN) {
|
||||
printf(" transparent_clipping = %s\n",
|
||||
wopts.transparent_clipping == TRI_TRUE ? "true" : "false");
|
||||
nothing = false;
|
||||
}
|
||||
if (wopts.full_shadow != TRI_UNKNOWN) {
|
||||
printf(" full_shadow = %s\n",
|
||||
wopts.full_shadow == TRI_TRUE ? "true" : "false");
|
||||
nothing = false;
|
||||
}
|
||||
if (wopts.unredir != WINDOW_UNREDIR_INVALID) {
|
||||
const char *str = NULL;
|
||||
switch (wopts.unredir) {
|
||||
case WINDOW_UNREDIR_WHEN_POSSIBLE_ELSE_TERMINATE: str = "true"; break;
|
||||
case WINDOW_UNREDIR_TERMINATE: str = "false"; break;
|
||||
case WINDOW_UNREDIR_FORCED: str = "\"forced\""; break;
|
||||
case WINDOW_UNREDIR_PASSIVE: str = "\"passive\""; break;
|
||||
case WINDOW_UNREDIR_WHEN_POSSIBLE: str = "\"preferred\""; break;
|
||||
default: unreachable();
|
||||
}
|
||||
printf(" unredir = %s\n", str);
|
||||
nothing = false;
|
||||
}
|
||||
if (!safe_isnan(wopts.opacity)) {
|
||||
printf(" opacity = %f\n", wopts.opacity);
|
||||
nothing = false;
|
||||
}
|
||||
if (!safe_isnan(wopts.dim)) {
|
||||
printf(" dim = %f\n", wopts.dim);
|
||||
nothing = false;
|
||||
}
|
||||
if (wopts.corner_radius >= 0) {
|
||||
printf(" corner_radius = %d\n", wopts.corner_radius);
|
||||
nothing = false;
|
||||
}
|
||||
|
||||
char **animation_triggers = dynarr_new(char *, 0);
|
||||
for (int i = 0; i <= ANIMATION_TRIGGER_LAST; i++) {
|
||||
if (wopts.animations[i].script != NULL) {
|
||||
char *name = NULL;
|
||||
casprintf(&name, "\"%s\"", animation_trigger_names[i]);
|
||||
dynarr_push(animation_triggers, name);
|
||||
}
|
||||
}
|
||||
if (dynarr_len(animation_triggers) > 0) {
|
||||
char *animation_triggers_str = dynarr_join(animation_triggers, ", ");
|
||||
printf(" animations = { triggers = [%s]; }\n", animation_triggers_str);
|
||||
free(animation_triggers_str);
|
||||
nothing = false;
|
||||
} else {
|
||||
dynarr_free_pod(animation_triggers);
|
||||
}
|
||||
|
||||
if (nothing) {
|
||||
printf(" (nothing)\n");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,5 +3,14 @@
|
|||
|
||||
#pragma once
|
||||
#include <xcb/xcb.h>
|
||||
#include "wm/win.h"
|
||||
|
||||
struct x_connection;
|
||||
struct c2_state;
|
||||
struct options;
|
||||
|
||||
int inspect_main(int argc, char **argv, const char *config_file);
|
||||
xcb_window_t inspect_select_window(struct x_connection *c);
|
||||
void inspect_dump_window(const struct c2_state *state, const struct options *opts,
|
||||
const struct win *w);
|
||||
void inspect_dump_window_maybe_options(struct window_maybe_options wopts);
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
#include "compiler.h"
|
||||
#include "log.h"
|
||||
#include "utils/console.h"
|
||||
#include "utils/misc.h"
|
||||
|
||||
thread_local struct log *tls_logger;
|
||||
|
@ -274,7 +275,6 @@ static void file_logger_destroy(struct log_target *tgt) {
|
|||
free(tgt);
|
||||
}
|
||||
|
||||
#define ANSI(x) "\033[" x "m"
|
||||
static const char *terminal_colorize_begin(enum log_level level) {
|
||||
switch (level) {
|
||||
case LOG_LEVEL_TRACE: return ANSI("30;2");
|
||||
|
|
|
@ -59,6 +59,7 @@ struct picom_option {
|
|||
int has_arg;
|
||||
struct picom_arg arg;
|
||||
const char *help;
|
||||
const char *argv0;
|
||||
};
|
||||
|
||||
static bool set_flag(const struct picom_option * /*opt*/, const struct picom_arg *arg,
|
||||
|
@ -70,7 +71,7 @@ static bool set_flag(const struct picom_option * /*opt*/, const struct picom_arg
|
|||
static bool set_rule_flag(const struct picom_option *arg_opt, const struct picom_arg *arg,
|
||||
const char * /*arg_str*/, void *output) {
|
||||
auto opt = (struct options *)output;
|
||||
if (opt->rules != NULL) {
|
||||
if (!list_is_empty(&opt->rules)) {
|
||||
log_warn_both_style_of_rules(arg_opt->long_name);
|
||||
opt->has_both_style_of_rules = true;
|
||||
return true;
|
||||
|
@ -114,7 +115,7 @@ static bool store_float(const struct picom_option *opt, const struct picom_arg *
|
|||
static bool store_rule_float(const struct picom_option *arg_opt, const struct picom_arg *arg,
|
||||
const char *arg_str, void *output) {
|
||||
auto opt = (struct options *)output;
|
||||
if (opt->rules != NULL) {
|
||||
if (!list_is_empty(&opt->rules)) {
|
||||
log_warn_both_style_of_rules(arg_opt->long_name);
|
||||
opt->has_both_style_of_rules = true;
|
||||
return true;
|
||||
|
@ -147,12 +148,12 @@ static bool store_rules(const struct picom_option *arg_opt, const struct picom_a
|
|||
const char *arg_str, void *output) {
|
||||
const struct picom_rules_parser *parser = arg->user_data;
|
||||
struct options *opt = (struct options *)output;
|
||||
if (opt->rules != NULL) {
|
||||
if (!list_is_empty(&opt->rules)) {
|
||||
log_warn_both_style_of_rules(arg_opt->long_name);
|
||||
opt->has_both_style_of_rules = true;
|
||||
return true;
|
||||
}
|
||||
auto rules = (c2_lptr_t **)(output + arg->offset);
|
||||
auto rules = (struct list_node *)(output + arg->offset);
|
||||
if (!parser->parse_prefix) {
|
||||
return c2_parse(rules, arg_str, NULL) != NULL;
|
||||
}
|
||||
|
@ -452,6 +453,8 @@ static const struct picom_option picom_options[] = {
|
|||
"window is fullscreen based only on its size and coordinates."},
|
||||
[804] = {"realtime" , ENABLE(use_realtime_scheduling) , "Enable realtime scheduling. This might reduce latency, but might also cause "
|
||||
"other issues. Disable this if you see the compositor being killed."},
|
||||
[805] = {"monitor" , ENABLE(inspect_monitor) , "For picom-inspect, run in a loop and dump information every time something "
|
||||
"changed about a window.", "picom-inspect"},
|
||||
|
||||
// Flags that takes an argument
|
||||
['r'] = {"shadow-radius" , INTEGER(shadow_radius, 0, INT_MAX) , "The blur radius for shadows. (default 12)"},
|
||||
|
@ -634,12 +637,19 @@ static void usage(const char *argv0, int ret) {
|
|||
line_wrap = window_size.ws_col;
|
||||
}
|
||||
|
||||
const char *basename = strrchr(argv0, '/') ? strrchr(argv0, '/') + 1 : argv0;
|
||||
|
||||
size_t help_indent = 0;
|
||||
for (size_t i = 0; i < ARR_SIZE(picom_options); i++) {
|
||||
if (picom_options[i].help == NULL) {
|
||||
// Hide options with no help message.
|
||||
continue;
|
||||
}
|
||||
if (picom_options[i].argv0 != NULL &&
|
||||
strcmp(picom_options[i].argv0, basename) != 0) {
|
||||
// Hide options that are not for this program.
|
||||
continue;
|
||||
}
|
||||
auto option_len = strlen(picom_options[i].long_name) + 2 + 4;
|
||||
if (picom_options[i].arg.name) {
|
||||
option_len += strlen(picom_options[i].arg.name) + 1;
|
||||
|
@ -654,6 +664,11 @@ static void usage(const char *argv0, int ret) {
|
|||
if (picom_options[i].help == NULL) {
|
||||
continue;
|
||||
}
|
||||
if (picom_options[i].argv0 != NULL &&
|
||||
strcmp(picom_options[i].argv0, basename) != 0) {
|
||||
// Hide options that are not for this program.
|
||||
continue;
|
||||
}
|
||||
size_t option_len = 8;
|
||||
fprintf(f, " ");
|
||||
if ((i > 'a' && i < 'z') || (i > 'A' && i < 'Z')) {
|
||||
|
@ -841,7 +856,7 @@ static bool sanitize_options(struct options *opt) {
|
|||
dynarr_clear(opt->all_scripts, script_ptr_deinit);
|
||||
}
|
||||
|
||||
if (opt->window_shader_fg || opt->window_shader_fg_rules) {
|
||||
if (opt->window_shader_fg || !list_is_empty(&opt->window_shader_fg_rules)) {
|
||||
log_warn("The new shader interface is not supported by the "
|
||||
"legacy glx backend. You may want to use "
|
||||
"--glx-fshader-win instead.");
|
||||
|
@ -943,10 +958,15 @@ bool get_cfg(options_t *opt, int argc, char *const *argv) {
|
|||
int o = 0, longopt_idx = -1;
|
||||
bool failed = false;
|
||||
optind = 1;
|
||||
const char *basename = strrchr(argv[0], '/') ? strrchr(argv[0], '/') + 1 : argv[0];
|
||||
while (-1 != (o = getopt_long(argc, argv, shortopts, longopts, &longopt_idx))) {
|
||||
if (o == '?' || o == ':' || picom_options[o].arg.handler == NULL) {
|
||||
usage(argv[0], 1);
|
||||
failed = true;
|
||||
} else if (picom_options[o].argv0 != NULL &&
|
||||
strcmp(picom_options[o].argv0, basename) != 0) {
|
||||
log_error("Invalid option %s", argv[optind - 1]);
|
||||
failed = true;
|
||||
} else if (!picom_options[o].arg.handler(
|
||||
&picom_options[o], &picom_options[o].arg, optarg, opt)) {
|
||||
failed = true;
|
||||
|
@ -1008,27 +1028,27 @@ bool get_cfg(options_t *opt, int argc, char *const *argv) {
|
|||
|
||||
void options_postprocess_c2_lists(struct c2_state *state, struct x_connection *c,
|
||||
struct options *option) {
|
||||
if (option->rules) {
|
||||
if (!c2_list_postprocess(state, c->c, option->rules)) {
|
||||
if (!list_is_empty(&option->rules)) {
|
||||
if (!c2_list_postprocess(state, c->c, &option->rules)) {
|
||||
log_error("Post-processing of rules failed, some of your rules "
|
||||
"might not work");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(c2_list_postprocess(state, c->c, option->unredir_if_possible_blacklist) &&
|
||||
c2_list_postprocess(state, c->c, option->paint_blacklist) &&
|
||||
c2_list_postprocess(state, c->c, option->shadow_blacklist) &&
|
||||
c2_list_postprocess(state, c->c, option->shadow_clip_list) &&
|
||||
c2_list_postprocess(state, c->c, option->fade_blacklist) &&
|
||||
c2_list_postprocess(state, c->c, option->blur_background_blacklist) &&
|
||||
c2_list_postprocess(state, c->c, option->invert_color_list) &&
|
||||
c2_list_postprocess(state, c->c, option->window_shader_fg_rules) &&
|
||||
c2_list_postprocess(state, c->c, option->opacity_rules) &&
|
||||
c2_list_postprocess(state, c->c, option->rounded_corners_blacklist) &&
|
||||
c2_list_postprocess(state, c->c, option->corner_radius_rules) &&
|
||||
c2_list_postprocess(state, c->c, option->focus_blacklist) &&
|
||||
c2_list_postprocess(state, c->c, option->transparent_clipping_blacklist))) {
|
||||
if (!(c2_list_postprocess(state, c->c, &option->unredir_if_possible_blacklist) &&
|
||||
c2_list_postprocess(state, c->c, &option->paint_blacklist) &&
|
||||
c2_list_postprocess(state, c->c, &option->shadow_blacklist) &&
|
||||
c2_list_postprocess(state, c->c, &option->shadow_clip_list) &&
|
||||
c2_list_postprocess(state, c->c, &option->fade_blacklist) &&
|
||||
c2_list_postprocess(state, c->c, &option->blur_background_blacklist) &&
|
||||
c2_list_postprocess(state, c->c, &option->invert_color_list) &&
|
||||
c2_list_postprocess(state, c->c, &option->window_shader_fg_rules) &&
|
||||
c2_list_postprocess(state, c->c, &option->opacity_rules) &&
|
||||
c2_list_postprocess(state, c->c, &option->rounded_corners_blacklist) &&
|
||||
c2_list_postprocess(state, c->c, &option->corner_radius_rules) &&
|
||||
c2_list_postprocess(state, c->c, &option->focus_blacklist) &&
|
||||
c2_list_postprocess(state, c->c, &option->transparent_clipping_blacklist))) {
|
||||
log_error("Post-processing of conditionals failed, some of your "
|
||||
"rules might not work");
|
||||
}
|
||||
|
|
28
src/picom.c
28
src/picom.c
|
@ -1986,10 +1986,6 @@ err:
|
|||
return true;
|
||||
}
|
||||
|
||||
static bool load_shader_source_for_condition(const c2_lptr_t *cond, void *data) {
|
||||
return load_shader_source(data, c2_list_get_data(cond));
|
||||
}
|
||||
|
||||
static struct window_options win_options_from_config(const struct options *opts) {
|
||||
struct window_options ret = {
|
||||
.blur_background = opts->blur_method != BLUR_METHOD_NONE,
|
||||
|
@ -2193,6 +2189,17 @@ static session_t *session_init(int argc, char **argv, Display *dpy,
|
|||
return NULL;
|
||||
}
|
||||
|
||||
const char *basename = strrchr(argv[0], '/') ? strrchr(argv[0], '/') + 1 : argv[0];
|
||||
|
||||
if (strcmp(basename, "picom-inspect") == 0) {
|
||||
ps->o.backend = backend_find("dummy");
|
||||
ps->o.print_diagnostics = false;
|
||||
ps->o.dbus = false;
|
||||
if (!ps->o.inspect_monitor) {
|
||||
ps->o.inspect_win = inspect_select_window(&ps->c);
|
||||
}
|
||||
}
|
||||
|
||||
ps->window_options_default = win_options_from_config(&ps->o);
|
||||
|
||||
if (ps->o.window_shader_fg) {
|
||||
|
@ -2227,9 +2234,11 @@ static session_t *session_init(int argc, char **argv, Display *dpy,
|
|||
options_postprocess_c2_lists(ps->c2_state, &ps->c, &ps->o);
|
||||
|
||||
// Load shader source file specified in the shader rules
|
||||
if (c2_list_foreach(ps->o.window_shader_fg_rules, load_shader_source_for_condition, ps)) {
|
||||
log_error("Failed to load shader source file for some of the window "
|
||||
"shader rules");
|
||||
c2_condition_list_foreach(&ps->o.window_shader_fg_rules, i) {
|
||||
if (!load_shader_source(ps, c2_condition_get_data(i))) {
|
||||
log_error("Failed to load shader source file for some of the "
|
||||
"window shader rules");
|
||||
}
|
||||
}
|
||||
if (load_shader_source(ps, ps->o.window_shader_fg)) {
|
||||
log_error("Failed to load window shader source file");
|
||||
|
@ -2734,11 +2743,6 @@ int PICOM_MAIN(int argc, char **argv) {
|
|||
return exit_code;
|
||||
}
|
||||
|
||||
char *exe_name = basename(argv[0]);
|
||||
if (strcmp(exe_name, "picom-inspect") == 0) {
|
||||
return inspect_main(argc, argv, config_file);
|
||||
}
|
||||
|
||||
int pfds[2];
|
||||
if (need_fork) {
|
||||
if (pipe2(pfds, O_CLOEXEC)) {
|
||||
|
|
6
src/utils/console.h
Normal file
6
src/utils/console.h
Normal file
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
/// Generate ANSI escape code
|
||||
#define ANSI(x) "\033[" x "m"
|
||||
/// Create a string that will print `str` in bold when output to terminal
|
||||
#define BOLD(str) "\033[1m" str "\033[0m"
|
84
src/wm/win.c
84
src/wm/win.c
|
@ -24,10 +24,12 @@
|
|||
#include "compiler.h"
|
||||
#include "config.h"
|
||||
#include "dbus.h"
|
||||
#include "inspect.h"
|
||||
#include "log.h"
|
||||
#include "picom.h"
|
||||
#include "region.h"
|
||||
#include "render.h"
|
||||
#include "utils/console.h"
|
||||
#include "utils/misc.h"
|
||||
#include "x.h"
|
||||
|
||||
|
@ -109,7 +111,7 @@ static bool win_is_focused(session_t *ps, struct win *w) {
|
|||
(ps->o.mark_wmwin_focused && is_wmwin) ||
|
||||
(ps->o.mark_ovredir_focused && wm_ref_client_of(w->tree_ref) == NULL && !is_wmwin) ||
|
||||
(w->a.map_state == XCB_MAP_STATE_VIEWABLE &&
|
||||
c2_match(ps->c2_state, w, ps->o.focus_blacklist, NULL))) {
|
||||
c2_match(ps->c2_state, w, &ps->o.focus_blacklist, NULL))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -846,7 +848,7 @@ static void win_determine_shadow(session_t *ps, struct win *w) {
|
|||
if (!ps->o.wintype_option[index_of_lowest_one(w->window_types)].shadow) {
|
||||
log_debug("Shadow disabled by wintypes");
|
||||
w->options.shadow = TRI_FALSE;
|
||||
} else if (c2_match(ps->c2_state, w, ps->o.shadow_blacklist, NULL)) {
|
||||
} else if (c2_match(ps->c2_state, w, &ps->o.shadow_blacklist, NULL)) {
|
||||
log_debug("Shadow disabled by shadow-exclude");
|
||||
w->options.shadow = TRI_FALSE;
|
||||
} else if (ps->o.shadow_ignore_shaped && w->bounding_shaped && !w->rounded_corners) {
|
||||
|
@ -892,7 +894,7 @@ bool win_update_prop_fullscreen(struct x_connection *c, const struct atom *atoms
|
|||
static void win_determine_clip_shadow_above(session_t *ps, struct win *w) {
|
||||
bool should_crop =
|
||||
(ps->o.wintype_option[index_of_lowest_one(w->window_types)].clip_shadow_above ||
|
||||
c2_match(ps->c2_state, w, ps->o.shadow_clip_list, NULL));
|
||||
c2_match(ps->c2_state, w, &ps->o.shadow_clip_list, NULL));
|
||||
w->options.clip_shadow_above = should_crop ? TRI_TRUE : TRI_UNKNOWN;
|
||||
}
|
||||
|
||||
|
@ -905,7 +907,7 @@ static void win_determine_invert_color(session_t *ps, struct win *w) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (c2_match(ps->c2_state, w, ps->o.invert_color_list, NULL)) {
|
||||
if (c2_match(ps->c2_state, w, &ps->o.invert_color_list, NULL)) {
|
||||
w->options.invert_color = TRI_TRUE;
|
||||
}
|
||||
}
|
||||
|
@ -925,7 +927,7 @@ static void win_determine_blur_background(session_t *ps, struct win *w) {
|
|||
if (!ps->o.wintype_option[index_of_lowest_one(w->window_types)].blur_background) {
|
||||
log_debug("Blur background disabled by wintypes");
|
||||
w->options.blur_background = TRI_FALSE;
|
||||
} else if (c2_match(ps->c2_state, w, ps->o.blur_background_blacklist, NULL)) {
|
||||
} else if (c2_match(ps->c2_state, w, &ps->o.blur_background_blacklist, NULL)) {
|
||||
log_debug("Blur background disabled by blur-background-exclude");
|
||||
w->options.blur_background = TRI_FALSE;
|
||||
}
|
||||
|
@ -937,13 +939,14 @@ static void win_determine_blur_background(session_t *ps, struct win *w) {
|
|||
*/
|
||||
static void win_determine_rounded_corners(session_t *ps, struct win *w) {
|
||||
void *radius_override = NULL;
|
||||
bool blacklisted = c2_match(ps->c2_state, w, ps->o.rounded_corners_blacklist, NULL);
|
||||
bool blacklisted = c2_match(ps->c2_state, w, &ps->o.rounded_corners_blacklist, NULL);
|
||||
if (blacklisted) {
|
||||
w->options.corner_radius = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
bool matched = c2_match(ps->c2_state, w, ps->o.corner_radius_rules, &radius_override);
|
||||
bool matched =
|
||||
c2_match(ps->c2_state, w, &ps->o.corner_radius_rules, &radius_override);
|
||||
if (matched) {
|
||||
log_debug("Window %#010x (%s) matched corner rule! %d", win_id(w),
|
||||
w->name, (int)(long)radius_override);
|
||||
|
@ -978,7 +981,7 @@ static void win_determine_fg_shader(session_t *ps, struct win *w) {
|
|||
|
||||
void *val = NULL;
|
||||
w->options.shader = NULL;
|
||||
if (c2_match(ps->c2_state, w, ps->o.window_shader_fg_rules, &val)) {
|
||||
if (c2_match(ps->c2_state, w, &ps->o.window_shader_fg_rules, &val)) {
|
||||
struct shader_info *shader = NULL;
|
||||
HASH_FIND_STR(ps->shaders, val, shader);
|
||||
w->options.shader = shader;
|
||||
|
@ -995,28 +998,32 @@ void win_update_opacity_rule(session_t *ps, struct win *w) {
|
|||
|
||||
double opacity = NAN;
|
||||
void *val = NULL;
|
||||
if (c2_match(ps->c2_state, w, ps->o.opacity_rules, &val)) {
|
||||
if (c2_match(ps->c2_state, w, &ps->o.opacity_rules, &val)) {
|
||||
opacity = ((double)(long)val) / 100.0;
|
||||
}
|
||||
|
||||
w->options.opacity = opacity;
|
||||
}
|
||||
|
||||
struct win_update_rule_params {
|
||||
struct win *w;
|
||||
struct session *ps;
|
||||
struct window_maybe_options options;
|
||||
};
|
||||
|
||||
static bool win_update_rule(const c2_lptr_t *rule, void *args) {
|
||||
auto params = (struct win_update_rule_params *)args;
|
||||
static bool
|
||||
win_update_rule(struct session *ps, struct win *w, const c2_condition *rule, bool inspect) {
|
||||
void *pdata = NULL;
|
||||
if (!c2_match_one(params->ps->c2_state, params->w, rule, &pdata)) {
|
||||
if (inspect) {
|
||||
printf(" %s ... ", c2_condition_to_str(rule));
|
||||
}
|
||||
bool matched = c2_match_one(ps->c2_state, w, rule, &pdata);
|
||||
if (inspect) {
|
||||
printf("%s\n", matched ? ANSI("1;32") "matched\033[0m" : "not matched");
|
||||
}
|
||||
if (!matched) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto wopts_next = (struct window_maybe_options *)pdata;
|
||||
params->options = win_maybe_options_fold(params->options, *wopts_next);
|
||||
if (inspect) {
|
||||
inspect_dump_window_maybe_options(*wopts_next);
|
||||
}
|
||||
w->options = win_maybe_options_fold(*wopts_next, w->options);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -1027,14 +1034,21 @@ static bool win_update_rule(const c2_lptr_t *rule, void *args) {
|
|||
*/
|
||||
void win_on_factor_change(session_t *ps, struct win *w) {
|
||||
auto wid = win_client_id(w, /*fallback_to_self=*/true);
|
||||
bool inspect = (ps->o.inspect_win != XCB_NONE && win_id(w) == ps->o.inspect_win) ||
|
||||
ps->o.inspect_monitor;
|
||||
log_debug("Window %#010x, client %#010x (%s) factor change", win_id(w), wid, w->name);
|
||||
c2_window_state_update(ps->c2_state, &w->c2_state, ps->c.c, wid, win_id(w));
|
||||
// Focus and is_fullscreen needs to be updated first, as other rules might depend
|
||||
// on the focused state of the window
|
||||
win_update_is_fullscreen(ps, w);
|
||||
|
||||
if (ps->o.inspect_monitor) {
|
||||
printf("Window %#010x (Client %#010x):\n======\n\n", win_id(w),
|
||||
win_client_id(w, /*fallback_to_self=*/true));
|
||||
}
|
||||
|
||||
assert(w->window_types != 0);
|
||||
if (ps->o.rules == NULL) {
|
||||
if (list_is_empty(&ps->o.rules)) {
|
||||
bool focused = win_is_focused(ps, w);
|
||||
auto window_type = index_of_lowest_one(w->window_types);
|
||||
// Universal rules take precedence over wintype_option and
|
||||
|
@ -1057,11 +1071,11 @@ void win_on_factor_change(session_t *ps, struct win *w) {
|
|||
w->options.fade = TRI_UNKNOWN;
|
||||
w->options.transparent_clipping = TRI_UNKNOWN;
|
||||
if (w->a.map_state == XCB_MAP_STATE_VIEWABLE &&
|
||||
c2_match(ps->c2_state, w, ps->o.paint_blacklist, NULL)) {
|
||||
c2_match(ps->c2_state, w, &ps->o.paint_blacklist, NULL)) {
|
||||
w->options.paint = TRI_FALSE;
|
||||
}
|
||||
if (w->a.map_state == XCB_MAP_STATE_VIEWABLE &&
|
||||
c2_match(ps->c2_state, w, ps->o.unredir_if_possible_blacklist, NULL)) {
|
||||
c2_match(ps->c2_state, w, &ps->o.unredir_if_possible_blacklist, NULL)) {
|
||||
if (ps->o.wintype_option[window_type].redir_ignore) {
|
||||
w->options.unredir = WINDOW_UNREDIR_PASSIVE;
|
||||
} else {
|
||||
|
@ -1077,23 +1091,23 @@ void win_on_factor_change(session_t *ps, struct win *w) {
|
|||
w->options.unredir = WINDOW_UNREDIR_WHEN_POSSIBLE;
|
||||
}
|
||||
|
||||
if (c2_match(ps->c2_state, w, ps->o.fade_blacklist, NULL)) {
|
||||
if (c2_match(ps->c2_state, w, &ps->o.fade_blacklist, NULL)) {
|
||||
w->options.fade = TRI_FALSE;
|
||||
}
|
||||
if (c2_match(ps->c2_state, w, ps->o.transparent_clipping_blacklist, NULL)) {
|
||||
if (c2_match(ps->c2_state, w, &ps->o.transparent_clipping_blacklist, NULL)) {
|
||||
w->options.transparent_clipping = TRI_FALSE;
|
||||
}
|
||||
w->options.full_shadow =
|
||||
tri_from_bool(ps->o.wintype_option[window_type].full_shadow);
|
||||
} else {
|
||||
struct win_update_rule_params params = {
|
||||
.w = w,
|
||||
.ps = ps,
|
||||
.options = WIN_MAYBE_OPTIONS_DEFAULT,
|
||||
};
|
||||
w->options = WIN_MAYBE_OPTIONS_DEFAULT;
|
||||
assert(w->state == WSTATE_MAPPED);
|
||||
c2_list_foreach(ps->o.rules, win_update_rule, ¶ms);
|
||||
w->options = params.options;
|
||||
if (inspect) {
|
||||
printf("Checking " BOLD("window rules") ":\n");
|
||||
}
|
||||
c2_condition_list_foreach_rev(&ps->o.rules, i) {
|
||||
win_update_rule(ps, w, i, inspect);
|
||||
}
|
||||
if (safe_isnan(w->options.opacity) && w->has_opacity_prop) {
|
||||
w->options.opacity = ((double)w->opacity_prop) / OPAQUE;
|
||||
}
|
||||
|
@ -1115,6 +1129,14 @@ void win_on_factor_change(session_t *ps, struct win *w) {
|
|||
(win_client_id(w, /*fallback_to_self=*/false) == ps->debug_window))) {
|
||||
w->options.paint = TRI_FALSE;
|
||||
}
|
||||
|
||||
if (inspect) {
|
||||
inspect_dump_window(ps->c2_state, &ps->o, w);
|
||||
printf("\n");
|
||||
if (!ps->o.inspect_monitor) {
|
||||
quit(ps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Reference in a new issue