#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; char *name; 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; char *appid; bool fullscreen; bool floating; bool selmon; tll(struct dwl_tag *) tags; char *layout; }; enum LINE_MODE { LINE_MODE_0, LINE_MODE_TITLE, LINE_MODE_APPID, LINE_MODE_FULLSCREEN, LINE_MODE_FLOATING, LINE_MODE_SELMON, LINE_MODE_TAGS, LINE_MODE_LAYOUT, }; static void free_dwl_tag(struct dwl_tag *tag) { free(tag->name); free(tag); } static void destroy(struct module *module) { struct private *private = module->private; private->label->destroy(private->label); tll_free_and_free(private->tags, free_dwl_tag); free(private->dwl_info_filename); free(private->title); free(private->layout); free(private); module_default_destroy(module); } static char const * description(const 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_string(module, "appid", private->appid), 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_string(module, "name", it->item->name), 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 = 11, }; 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_string(module, "appid", private->appid), 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_string(module, "name", "0"), tag_new_bool(module, "selected", false), tag_new_bool(module, "empty", true), tag_new_bool(module, "urgent", false), }, .count = 11, }; 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_from_id(struct private *private, uint32_t id) { tll_foreach(private->tags, it) { if (it->item->id == id) return it->item; } assert(false); /* unreachable */ return NULL; } 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; /* Update the title here, to avoid allocate and free memory on * every iteration (the line is separated by spaces, then we * join it again) a bit suboptimal, isn't it?) */ free(private->title); private->title = strdup(save_pointer); break; } else if (strcmp(string, "appid") == 0) { line_mode = LINE_MODE_APPID; /* Update the appid here, same as the title. */ free(private->appid); private->appid = strdup(save_pointer); break; } 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 information */ 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_from_id(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: case LINE_MODE_APPID: assert(false); /* unreachable */ 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, struct yml_node const *name_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 yml_list_iter list = {0}; if (name_of_tags) list = yml_list_iter(name_of_tags); for (int i = 1; i <= number_of_tags; i++) { struct dwl_tag *dwl_tag = calloc(1, sizeof(struct dwl_tag)); dwl_tag->id = i; if (list.node) { dwl_tag->name = strdup(yml_value_as_string(list.node)); yml_list_next(&list); } else if (asprintf(&dwl_tag->name, "%d", i) < 0) { LOG_ERRNO("asprintf"); } tll_push_back(private->tags, dwl_tag); } 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 *name_of_tags = yml_get_value(node, "name-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), name_of_tags, yml_value_as_string(dwl_info_filename)); } static bool verify_names(keychain_t *keychain, const struct yml_node *node) { if (!yml_is_list(node)) { LOG_ERR("%s: %s is not a list", conf_err_prefix(keychain, node), yml_value_as_string(node)); return false; } return conf_verify_list(keychain, node, &conf_verify_string); } 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}, {"name-of-tags", false, &verify_names}, {"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 *ntags_key = yml_get_key(node, "number-of-tags"); struct yml_node const *value = yml_get_value(node, "number-of-tags"); int number_of_tags = yml_value_as_int(value); if (number_of_tags == 0) { LOG_ERR("%s: %s must not be 0", conf_err_prefix(keychain, ntags_key), yml_value_as_string(ntags_key)); return false; } struct yml_node const *key = yml_get_key(node, "name-of-tags"); value = yml_get_value(node, "name-of-tags"); if (value && yml_list_length(value) != number_of_tags) { LOG_ERR("%s: %s must have the same number of elements that %s", conf_err_prefix(keychain, key), yml_value_as_string(key), yml_value_as_string(ntags_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