diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ff593b..38a0b93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,11 +32,13 @@ * mpd: `file` tag ([#219][219]). * pipewire: add a new module for pipewire ([#224][224]) * on-click: support `next`/`previous` mouse buttons ([#228][228]). +* dwl: add a new module for DWL ([#218][218]) [153]: https://codeberg.org/dnkl/yambar/issues/153 [159]: https://codeberg.org/dnkl/yambar/issues/159 [200]: https://codeberg.org/dnkl/yambar/issues/200 [202]: https://codeberg.org/dnkl/yambar/issues/202 +[218]: https://codeberg.org/dnkl/yambar/pulls/218 [219]: https://codeberg.org/dnkl/yambar/pulls/219 [223]: https://codeberg.org/dnkl/yambar/pulls/223 [224]: https://codeberg.org/dnkl/yambar/pulls/224 diff --git a/doc/meson.build b/doc/meson.build index 0d62cb5..e561ef3 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -6,7 +6,7 @@ scdoc_prog = find_program(scdoc.get_variable('scdoc'), native: true) foreach man_src : ['yambar.1.scd', 'yambar.5.scd', 'yambar-decorations.5.scd', 'yambar-modules-alsa.5.scd', 'yambar-modules-backlight.5.scd', 'yambar-modules-battery.5.scd', 'yambar-modules-clock.5.scd', - 'yambar-modules-disk-io.5.scd', + 'yambar-modules-disk-io.5.scd', 'yambar-modules-dwl.5.scd', 'yambar-modules-foreign-toplevel.5.scd', 'yambar-modules-i3.5.scd', 'yambar-modules-label.5.scd', 'yambar-modules-mpd.5.scd', 'yambar-modules-network.5.scd', diff --git a/doc/yambar-modules-dwl.5.scd b/doc/yambar-modules-dwl.5.scd new file mode 100644 index 0000000..4e41691 --- /dev/null +++ b/doc/yambar-modules-dwl.5.scd @@ -0,0 +1,95 @@ +yambar-modules-dwl(5) + +# NAME +dwl - This module provides information about dwl tags, and information. + +# DESCRIPTION + +This module provides a map of each tags present in dwl. + +Each tags has its _id_, its status (_selected_, _empty_, _urgent_) +and the global data like _title_, _fullscreen_, _floating_, +_selmon_, and _layout_). The tags start a 1. For needs where +you only want information about the global data and not the _tags_, +there is a tag with the id _0_ that contains only the global data. + +This module will track *only* the monitor where yambar was launched on. +If you have a multi monitor setup, please launch yambar on each of your +monitors. + +Please, be aware that only *one instance* of this module is supported. +Running multiple instances at the same time may result in +*undefined behavior*. + +# TAGS + +[[ *Name* +:[ *Type* +:[ *Description* +| id +: int +: Dwl tag id. +| selected +: bool +: True if the tag is currently selected. +| empty +: bool +: True if there are no windows in the tag. +| urgent +: bool +: True if the tag has the urgent flag set. +| title +: string +: The currently focused window's title. +| fullscreen +: bool +: True if there is a fullscreen window in the current tag. +| floating +: bool +: True if there is a floating window in the current tag. +| selmon +: bool +: True if the monitor is actually focused. +| layout +: string +: The actual layout name of the tag. + +# CONFIGURATION + +[[ *Name* +:[ *Type* +:[ *Req* +:[ *Description* +| number-of-tags +: int +: yes +: The number of defined tags in the dwl `config.def.h`. +| dwl-info-filename +: string +: yes +: The filepath to the log emitted by dwl when running. + +# EXAMPLES + +``` +bar: + left: + - dwl: + number-of-tags: 9 + dwl-info-filename: "/home/ogromny/dwl_info" + content: + list: + items: + - map: + conditions: + selected: {string: {text: "-> {id}"}} + ~empty: {string: {text: "{id}"}} + urgent: {string: {text: "=> {id} <="}} + # default tag + id == 0: {string: {text: "{layout} {title}"}} +``` + +# SEE ALSO + +*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) + diff --git a/meson.build b/meson.build index d480929..5c14fd4 100644 --- a/meson.build +++ b/meson.build @@ -135,6 +135,7 @@ yambar = executable( plugin_mpd_enabled? '-DPLUGIN_ENABLED_MPD':[], plugin_pulse_enabled? '-DPLUGIN_ENABLED_PULSE':[], plugin_pipewire_enabled? '-DPLUGIN_ENABLED_PIPEWIRE':[], + plugin_dwl_enabled? '-DPLUGIN_ENABLED_DWL':[], ], build_rpath: '$ORIGIN/modules:$ORIGIN/decorations:$ORIGIN/particles', export_dynamic: true, @@ -175,7 +176,8 @@ summary( { 'Music Player Daemon (MPD)': plugin_mpd_enabled, 'PulseAudio': plugin_pulse_enabled, - 'Pipewire': plugin_pipewire_enabled + 'Pipewire': plugin_pipewire_enabled, + 'DWL (dwm for wayland)': plugin_dwl_enabled, }, section: 'Optional modules', bool_yn: true diff --git a/meson_options.txt b/meson_options.txt index bcfce60..bd77f77 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -14,3 +14,6 @@ option( option( 'plugin-pipewire', type: 'feature', value: 'auto', description: 'Pipewire support') +option( + 'plugin-dwl', type: 'feature', value: 'auto', + description: 'DWL (dwm for wayland) support') diff --git a/modules/dwl.c b/modules/dwl.c new file mode 100644 index 0000000..6c8ccff --- /dev/null +++ b/modules/dwl.c @@ -0,0 +1,487 @@ +#include +#include +#include +#include +#include + +#define ARR_LEN(x) (sizeof((x)) / sizeof((x)[0])) + +#include "../config-verify.h" +#include "../config.h" +#include "../log.h" +#include "../module.h" +#include "../particles/dynlist.h" +#include "../plugin.h" + +#define LOG_MODULE "dwl" +#define LOG_ENABLE_DBG 0 + +struct dwl_tag { + int id; + bool selected; + bool empty; + bool urgent; +}; + +struct private +{ + struct particle *label; + + char const *monitor; + + unsigned int number_of_tags; + char *dwl_info_filename; + + /* dwl data */ + char *title; + bool fullscreen; + bool floating; + bool selmon; + tll(struct dwl_tag *) tags; + char *layout; +}; + +enum LINE_MODE { + LINE_MODE_0, + LINE_MODE_TITLE, + LINE_MODE_FULLSCREEN, + LINE_MODE_FLOATING, + LINE_MODE_SELMON, + LINE_MODE_TAGS, + LINE_MODE_LAYOUT, +}; + +static void +destroy(struct module *module) +{ + struct private *private = module->private; + private->label->destroy(private->label); + + tll_free_and_free(private->tags, free); + free(private->dwl_info_filename); + free(private->title); + free(private->layout); + free(private); + + module_default_destroy(module); +} + +static char const * +description(struct module *module) +{ + return "dwl"; +} + +static struct exposable * +content(struct module *module) +{ + struct private const *private = module->private; + mtx_lock(&module->lock); + + size_t i = 0; + /* + 1 for `default` tag */ + struct exposable *exposable[tll_length(private->tags) + 1]; + tll_foreach(private->tags, it) + { + struct tag_set tags = { + .tags = (struct tag*[]){ + tag_new_string(module, "title", private->title), + tag_new_bool(module, "fullscreen", private->fullscreen), + tag_new_bool(module, "floating", private->floating), + tag_new_bool(module, "selmon", private->selmon), + tag_new_string(module, "layout", private->layout), + tag_new_int(module, "id", it->item->id), + tag_new_bool(module, "selected", it->item->selected), + tag_new_bool(module, "empty", it->item->empty), + tag_new_bool(module, "urgent", it->item->urgent), + }, + .count = 9, + }; + exposable[i++] = private->label->instantiate(private->label, &tags); + tag_set_destroy(&tags); + } + + /* default tag (used for title, layout, etc) */ + struct tag_set tags = { + .tags = (struct tag*[]){ + tag_new_string(module, "title", private->title), + tag_new_bool(module, "fullscreen", private->fullscreen), + tag_new_bool(module, "floating", private->floating), + tag_new_bool(module, "selmon", private->selmon), + tag_new_string(module, "layout", private->layout), + tag_new_int(module, "id", 0), + tag_new_bool(module, "selected", false), + tag_new_bool(module, "empty", true), + tag_new_bool(module, "urgent", false), + }, + .count = 9, + }; + exposable[i++] = private->label->instantiate(private->label, &tags); + tag_set_destroy(&tags); + + mtx_unlock(&module->lock); + return dynlist_exposable_new(exposable, i, 0, 0); +} + +static struct dwl_tag * +dwl_tag_find_or_create(struct private *private, uint32_t id) +{ + tll_foreach(private->tags, it) + { + if (it->item->id == id) + return it->item; + } + + /* No need to order the tag, `print_status` from dwl already orders tags */ + struct dwl_tag *dwl_tag = calloc(1, sizeof(struct dwl_tag)); + dwl_tag->id = id; + tll_push_back(private->tags, dwl_tag); + return dwl_tag; +} + +static void +process_line(char *line, struct module *module) +{ + struct private *private = module->private; + enum LINE_MODE line_mode = LINE_MODE_0; + + /* Remove \n */ + line[strcspn(line, "\n")] = '\0'; + + /* Split line by space */ + size_t index = 1; + char *save_pointer = NULL; + char *string = strtok_r(line, " ", &save_pointer); + while (string != NULL) { + /* dwl logs are formatted like this + * $1 -> monitor + * $2 -> action + * $3 -> arg1 + * $4 -> arg2 + * ... */ + + /* monitor */ + if (index == 1) { + /* Not our monitor */ + if (strcmp(string, private->monitor) != 0) + break; + } + /* action */ + else if (index == 2) { + if (strcmp(string, "title") == 0) + line_mode = LINE_MODE_TITLE; + else if (strcmp(string, "fullscreen") == 0) + line_mode = LINE_MODE_FULLSCREEN; + else if (strcmp(string, "floating") == 0) + line_mode = LINE_MODE_FLOATING; + else if (strcmp(string, "selmon") == 0) + line_mode = LINE_MODE_SELMON; + else if (strcmp(string, "tags") == 0) + line_mode = LINE_MODE_TAGS; + else if (strcmp(string, "layout") == 0) + line_mode = LINE_MODE_LAYOUT; + else { + LOG_WARN("UNKNOWN action, please open an issue on https://codeberg.org/dnkl/yambar"); + return; + } + } + /* args */ + else { + if (line_mode == LINE_MODE_TAGS) { + static uint32_t occupied, selected, client_tags, urgent; + static uint32_t *target = NULL; + + /* dwl tags action log are formatted like this + * $3 -> occupied + * $4 -> tags + * $5 -> clientTags (not needed) + * $6 -> urgent */ + if (index == 3) + target = &occupied; + else if (index == 4) + target = &selected; + else if (index == 5) + target = &client_tags; + else if (index == 6) + target = &urgent; + + /* No need to check error IMHO */ + *target = strtoul(string, NULL, 10); + + /* Populate informations */ + if (index == 6) { + for (size_t id = 1; id <= private->number_of_tags; ++id) { + uint32_t mask = 1 << (id - 1); + + struct dwl_tag *dwl_tag = dwl_tag_find_or_create(private, id); + dwl_tag->selected = mask & selected; + dwl_tag->empty = !(mask & occupied); + dwl_tag->urgent = mask & urgent; + } + } + } else + switch (line_mode) { + case LINE_MODE_TITLE: + free(private->title); + private->title = strdup(string); + break; + case LINE_MODE_FULLSCREEN: + private->fullscreen = (strcmp(string, "0") != 0); + break; + case LINE_MODE_FLOATING: + private->floating = (strcmp(string, "0") != 0); + break; + case LINE_MODE_SELMON: + private->selmon = (strcmp(string, "0") != 0); + break; + case LINE_MODE_LAYOUT: + free(private->layout); + private->layout = strdup(string); + break; + default:; + assert(false); /* unreachable */ + } + } + + string = strtok_r(NULL, " ", &save_pointer); + ++index; + } +} + +static int +file_read_content(FILE *file, struct module *module) +{ + static char buffer[1024]; + + errno = 0; + while (fgets(buffer, ARR_LEN(buffer), file) != NULL) + process_line(buffer, module); + + fseek(file, 0, SEEK_END); + + /* Check whether error has been */ + if (ferror(file) != 0) { + LOG_ERRNO("unable to read file's content."); + return 1; + } + + return 0; +} + +static void +file_seek_to_last_n_lines(FILE *file, int number_of_lines) +{ + if (number_of_lines == 0 || file == NULL) + return; + + fseek(file, 0, SEEK_END); + + long position = ftell(file); + while (position > 0) { + /* Cannot go less than position 0 */ + if (fseek(file, --position, SEEK_SET) == EINVAL) + break; + + if (fgetc(file) == '\n') + if (number_of_lines-- == 0) + break; + } +} + +static int +run_init(int *inotify_fd, int *inotify_wd, FILE **file, char *dwl_info_filename) +{ + *inotify_fd = inotify_init(); + if (*inotify_fd == -1) { + LOG_ERRNO("unable to create inotify fd."); + return -1; + } + + *inotify_wd = inotify_add_watch(*inotify_fd, dwl_info_filename, IN_MODIFY); + if (*inotify_wd == -1) { + close(*inotify_fd); + LOG_ERRNO("unable to add watch to inotify fd."); + return 1; + } + + *file = fopen(dwl_info_filename, "r"); + if (*file == NULL) { + inotify_rm_watch(*inotify_fd, *inotify_wd); + close(*inotify_fd); + LOG_ERRNO("unable to open file."); + return 1; + } + + return 0; +} + +static int +run_clean(int inotify_fd, int inotify_wd, FILE *file) +{ + if (inotify_fd != -1) { + if (inotify_wd != -1) + inotify_rm_watch(inotify_fd, inotify_wd); + close(inotify_fd); + } + + if (file != NULL) { + if (fclose(file) == EOF) { + LOG_ERRNO("unable to close file."); + return 1; + } + } + + return 0; +}; + +static int +run(struct module *module) +{ + struct private *private = module->private; + + /* Ugly, but I didn't find better way for waiting + * the monitor's name to be set */ + do { + private->monitor = module->bar->output_name(module->bar); + usleep(50); + } while (private->monitor == NULL); + + int inotify_fd = -1, inotify_wd = -1; + FILE *file = NULL; + if (run_init(&inotify_fd, &inotify_wd, &file, private->dwl_info_filename) != 0) + return 1; + + /* Dwl output is 6 lines per monitor, so let's assume that nobody has + * more than 5 monitors (6 * 5 = 30) */ + mtx_lock(&module->lock); + file_seek_to_last_n_lines(file, 30); + if (file_read_content(file, module) != 0) { + mtx_unlock(&module->lock); + return run_clean(inotify_fd, inotify_wd, file); + } + mtx_unlock(&module->lock); + + module->bar->refresh(module->bar); + + while (true) { + struct pollfd fds[] = { + (struct pollfd){.fd = module->abort_fd, .events = POLLIN}, + (struct pollfd){.fd = inotify_fd, .events = POLLIN}, + }; + + if (poll(fds, ARR_LEN(fds), -1) == -1) { + if (errno == EINTR) + continue; + + LOG_ERRNO("unable to poll."); + break; + } + + if (fds[0].revents & POLLIN) + break; + + /* fds[1] (inotify_fd) must be POLLIN otherwise issue happen'd */ + if (!(fds[1].revents & POLLIN)) { + LOG_ERR("expected POLLIN revent"); + break; + } + + /* Block until event */ + static char buffer[1024]; + ssize_t length = read(inotify_fd, buffer, ARR_LEN(buffer)); + + if (length == 0) + break; + + if (length == -1) { + if (errno == EAGAIN) + continue; + + LOG_ERRNO("unable to read %s", private->dwl_info_filename); + break; + } + + mtx_lock(&module->lock); + if (file_read_content(file, module) != 0) { + mtx_unlock(&module->lock); + break; + } + mtx_unlock(&module->lock); + + module->bar->refresh(module->bar); + } + + return run_clean(inotify_fd, inotify_wd, file); +} + +static struct module * +dwl_new(struct particle *label, int number_of_tags, char const *dwl_info_filename) +{ + struct private *private = calloc(1, sizeof(struct private)); + private->label = label; + private->number_of_tags = number_of_tags; + private->dwl_info_filename = strdup(dwl_info_filename); + + struct module *module = module_common_new(); + module->private = private; + module->run = &run; + module->destroy = &destroy; + module->content = &content; + module->description = &description; + + return module; +} + +static struct module * +from_conf(struct yml_node const *node, struct conf_inherit inherited) +{ + struct yml_node const *content = yml_get_value(node, "content"); + struct yml_node const *number_of_tags = yml_get_value(node, "number-of-tags"); + struct yml_node const *dwl_info_filename = yml_get_value(node, "dwl-info-filename"); + + return dwl_new(conf_to_particle(content, inherited), yml_value_as_int(number_of_tags), + yml_value_as_string(dwl_info_filename)); +} + +static bool +verify_conf(keychain_t *keychain, struct yml_node const *node) +{ + + static struct attr_info const attrs[] = { + {"number-of-tags", true, &conf_verify_unsigned}, + {"dwl-info-filename", true, &conf_verify_string}, + MODULE_COMMON_ATTRS, + }; + + if (!conf_verify_dict(keychain, node, attrs)) + return false; + + /* No need to check whether is `number_of_tags` is a int + * because `conf_verify_unsigned` already did it */ + struct yml_node const *key = yml_get_key(node, "number-of-tags"); + struct yml_node const *value = yml_get_value(node, "number-of-tags"); + if (yml_value_as_int(value) == 0) { + LOG_ERR("%s: %s must not be 0", conf_err_prefix(keychain, key), yml_value_as_string(key)); + return false; + } + + /* No need to check whether is `dwl_info_filename` is a string + * because `conf_verify_string` already did it */ + key = yml_get_key(node, "dwl-info-filename"); + value = yml_get_value(node, "dwl-info-filename"); + if (strlen(yml_value_as_string(value)) == 0) { + LOG_ERR("%s: %s must not be empty", conf_err_prefix(keychain, key), yml_value_as_string(key)); + return false; + } + + return true; +} + +struct module_iface const module_dwl_iface = { + .verify_conf = &verify_conf, + .from_conf = &from_conf, +}; + +#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES) +extern struct module_iface const iface __attribute__((weak, alias("module_dwl_iface"))); +#endif diff --git a/modules/meson.build b/modules/meson.build index ddfd747..56a4757 100644 --- a/modules/meson.build +++ b/modules/meson.build @@ -16,6 +16,8 @@ plugin_pipewire_enabled = pipewire.found() pulse = dependency('libpulse', required: get_option('plugin-pulse')) plugin_pulse_enabled = pulse.found() +plugin_dwl_enabled = get_option('plugin-dwl').allowed() + # Module name -> (source-list, dep-list) mod_data = { 'alsa': [[], [m, alsa]], @@ -33,6 +35,10 @@ mod_data = { 'sway-xkb': [['i3-common.c', 'i3-common.h'], [dynlist, json]], } +if plugin_dwl_enabled + mod_data += {'dwl': [[], [dynlist]]} +endif + if plugin_mpd_enabled mod_data += {'mpd': [[], [mpd]]} endif diff --git a/plugin.c b/plugin.c index a17f1ae..6320434 100644 --- a/plugin.c +++ b/plugin.c @@ -37,6 +37,9 @@ EXTERN_MODULE(backlight); EXTERN_MODULE(battery); EXTERN_MODULE(clock); EXTERN_MODULE(disk_io); +#if defined(PLUGIN_ENABLED_DWL) +EXTERN_MODULE(dwl); +#endif EXTERN_MODULE(foreign_toplevel); EXTERN_MODULE(i3); EXTERN_MODULE(label); @@ -126,6 +129,9 @@ init(void) REGISTER_CORE_MODULE(battery, battery); REGISTER_CORE_MODULE(clock, clock); REGISTER_CORE_MODULE(disk-io, disk_io); +#if defined(PLUGIN_ENABLED_DWL) + REGISTER_CORE_MODULE(dwl, dwl); +#endif #if defined(HAVE_PLUGIN_foreign_toplevel) REGISTER_CORE_MODULE(foreign-toplevel, foreign_toplevel); #endif diff --git a/yml.c b/yml.c index 6afa8d6..b3f3d42 100644 --- a/yml.c +++ b/yml.c @@ -640,9 +640,11 @@ yml_is_list(const struct yml_node *node) return node->type == LIST; } - const struct yml_node * -yml_get_value(const struct yml_node *node, const char *_path) +static struct yml_node const * +yml_get_(struct yml_node const *node, char const *_path, bool value) { + /* value: true for value, false for key */ + if (node != NULL && node->type == ROOT) node = node->root.root; @@ -662,7 +664,11 @@ yml_get_value(const struct yml_node *node, const char *_path) if (strcmp(it->item.key->scalar.value, part) == 0) { if (next_part == NULL) { free(path); - return it->item.value; + + if (value) + return it->item.value; + else + return it->item.key; } node = it->item.value; @@ -675,6 +681,17 @@ yml_get_value(const struct yml_node *node, const char *_path) return NULL; } +const struct yml_node * +yml_get_value(const struct yml_node *node, const char *_path) +{ + return yml_get_(node, _path, true); +} + +struct yml_node const * +yml_get_key(struct yml_node const *node, char const *_path) { + return yml_get_(node, _path, false); +} + struct yml_list_iter yml_list_iter(const struct yml_node *list) { diff --git a/yml.h b/yml.h index 476d469..0e5eca4 100644 --- a/yml.h +++ b/yml.h @@ -13,6 +13,8 @@ bool yml_is_list(const struct yml_node *node); const struct yml_node *yml_get_value( const struct yml_node *node, const char *path); +const struct yml_node *yml_get_key( + struct yml_node const *node, char const *path); struct yml_list_iter { const struct yml_node *node;