From ee0eb7ccf735ad26593cb5a4804b746e273469b4 Mon Sep 17 00:00:00 2001 From: Patrick Gaskin Date: Mon, 22 Jun 2020 23:34:37 -0400 Subject: [PATCH] Implemented config file reloading and improved the config parser (closes #42) (closes #48) (#47) Menu: * Implemented menu item updating based on config file creation/deletion/modtime. * Fixed ordering and separators for dynamically added actions. * Simplified separator property setting. * Fixed issue with item ordering on every second update (it keeps track by the item index based on the original menu, which changes after we add our items, so we actually need to remove our items before we find the item to prepend our new items to). * Note: generators can't trigger updates yet, and are only updated when the config changes. Config: * Split config parsing and scanning into multiple steps. * No more memory leaks on error. * Properly handle memory allocation errors. * Cleaner code. * Less chance of making mistakes when adding features in the future. * Also made if statements easier to read. * Refactored config state and list management into separate functions. * Extracted parsers for each config type into separate functions. * Cleaned up whitespace. * Rearranged variables. * Fixed a few memory leaks and use-after-free issues for errors in the config parser. * Fixed use-after-free of line in config parser. * Moved config file filtering into scandir. See #47 for details. --- src/config.c | 706 ++++++++++++++++++++++++++++++++++++--------------- src/config.h | 24 +- src/init.c | 141 ++++++++-- src/init.h | 28 ++ src/menu.cc | 65 +++-- src/menu.h | 9 +- 6 files changed, 704 insertions(+), 269 deletions(-) create mode 100644 src/init.h diff --git a/src/config.c b/src/config.c index 57e96a9..34cfeda 100644 --- a/src/config.c +++ b/src/config.c @@ -8,6 +8,7 @@ #include #include #include +#include #include "action.h" #include "config.h" @@ -15,6 +16,140 @@ #include "menu.h" #include "util.h" +struct nm_config_file_t { + char *path; + struct timespec mtime; + nm_config_file_t *next; +}; + + +// nm_config_files_filter skips special files, including: +// - dotfiles +// - vim: .*~, .*.s?? (unix only, usually swp or swo), *.swp, *.swo +// - gedit: .*~ +// - emacs: .*~, #*# +// - kate: .*.kate-swp +// - macOS: .DS_Store*, .Spotlight-V*, ._* +// - Windows: [Tt]humbs.db, desktop.ini +static int nm_config_files_filter(const struct dirent *de) { + const char *bn = de->d_name; + char *ex = strrchr(bn, '.'); + ex = (ex && ex != bn && bn[0] != '.') ? ex : NULL; + char lc = bn[strlen(bn)-1]; + if ((bn[0] == '.') || + (lc == '~') || + (bn[0] == '#' && lc == '#') || + (ex && (!strcmp(ex, ".swo") || !strcmp(ex, ".swp"))) || + (!strcmp(&bn[1], "humbs.db") && tolower(bn[0]) == 't') || + (!strcmp(bn, "desktop.ini"))) { + NM_LOG("config: skipping %s/%s because it's a special file", NM_CONFIG_DIR, de->d_name); + return 0; + } + return 1; +} + +nm_config_file_t *nm_config_files(char **err_out) { + #define NM_ERR_RET NULL + + nm_config_file_t *cfs = NULL, *cfc = NULL; + + struct dirent **nl; + int n = scandir(NM_CONFIG_DIR, &nl, nm_config_files_filter, alphasort); + NM_ASSERT(n != -1, "could not scan config dir: %s", strerror(errno)); + + for (int i = 0; i < n; i++) { + struct dirent *de = nl[i]; + + char *fn; + if (asprintf(&fn, "%s/%s", NM_CONFIG_DIR, de->d_name) == -1) + fn = NULL; + + struct stat statbuf; + if (!fn || stat(fn, &statbuf)) { + while (cfs) { + nm_config_file_t *tmp = cfs->next; + free(cfs); + cfs = tmp; + } + if (!fn) + NM_RETURN_ERR("could not build full path for config file"); + free(fn); + NM_RETURN_ERR("could not stat %s/%s", NM_CONFIG_DIR, de->d_name); + } + + // skip it if it isn't a file + if (de->d_type != DT_REG && !S_ISREG(statbuf.st_mode)) { + NM_LOG("config: skipping %s because not a regular file", fn); + free(fn); + continue; + } + + if (cfc) { + cfc->next = calloc(1, sizeof(nm_config_file_t)); + cfc = cfc->next; + } else { + cfs = calloc(1, sizeof(nm_config_file_t)); + cfc = cfs; + } + + cfc->path = fn; + cfc->mtime = statbuf.st_mtim; + + free(de); + } + + free(nl); + + NM_RETURN_OK(cfs); + #undef NM_ERR_RET +} + +bool nm_config_files_update(nm_config_file_t **files, char **err_out) { + #define NM_ERR_RET false + NM_ASSERT(files, "files pointer must not be null"); + + nm_config_file_t *nfiles = nm_config_files(err_out); + if (*err_out) + return NM_ERR_RET; + + if (!*files) { + *files = nfiles; + NM_RETURN_OK(true); + } + + bool ch = false; + nm_config_file_t *op = *files; + nm_config_file_t *np = nfiles; + + while (op && np) { + if (strcmp(op->path, np->path) || op->mtime.tv_sec != np->mtime.tv_sec || op->mtime.tv_nsec != np->mtime.tv_nsec) { + ch = true; + break; + } + op = op->next; + np = np->next; + } + + if (ch || op || np) { + nm_config_files_free(*files); + *files = nfiles; + NM_RETURN_OK(true); + } else { + nm_config_files_free(nfiles); + NM_RETURN_OK(false); + } + + #undef NM_ERR_RET +} + +void nm_config_files_free(nm_config_file_t *files) { + while (files) { + nm_config_file_t *tmp = files->next; + free(files); + files = tmp; + } +} + typedef enum { NM_CONFIG_TYPE_MENU_ITEM = 1, NM_CONFIG_TYPE_GENERATOR = 2, @@ -30,236 +165,161 @@ struct nm_config_t { nm_config_t *next; }; -static void nm_config_push_menu_item(nm_config_t **cfg, nm_menu_item_t *it) { - nm_config_t *tmp = calloc(1, sizeof(nm_config_t)); - tmp->type = NM_CONFIG_TYPE_MENU_ITEM; - tmp->value.menu_item = it; - tmp->next = *cfg; - *cfg = tmp; +// nm_config_parse__state_t contains the current state of the config parser. It +// should be initialized to zero. The nm_config_parse__append__* functions will +// deep-copy the item to append (to malloc'd memory). Each call will always +// leave the state consistent, even on error (i.e. it will always be safe to +// nm_config_free cfg_s). +typedef struct nm_config_parse__state_t { + nm_config_t *cfg_s; // config (first) + nm_config_t *cfg_c; // config (current) + + nm_menu_item_t *cfg_it_c; // menu item (current) + nm_menu_action_t *cfg_it_act_s; // menu action (first) + nm_menu_action_t *cfg_it_act_c; // menu action (current) + + nm_generator_t *cfg_gn_c; // generator (current) +} nm_config_parse__state_t; + +typedef enum nm_config_parse__append__ret_t { + NM_CONFIG_PARSE__APPEND__RET_OK = 0, + NM_CONFIG_PARSE__APPEND__RET_ALLOC_ERROR = 1, + NM_CONFIG_PARSE__APPEND__RET_ACTION_MUST_BE_AFTER_ITEM = 2, +} nm_config_parse__append__ret_t; + +static nm_config_parse__append__ret_t nm_config_parse__append_item(nm_config_parse__state_t *restrict state, nm_menu_item_t *const restrict it); // note: action pointer will be ignored (add it with append_action) +static nm_config_parse__append__ret_t nm_config_parse__append_action(nm_config_parse__state_t *restrict state, nm_menu_action_t *const restrict act); // note: next pointer will be ignored (add another one by calling this again) +static nm_config_parse__append__ret_t nm_config_parse__append_generator(nm_config_parse__state_t *restrict state, nm_generator_t *const restrict gn); + +static const char* nm_config_parse__strerror(nm_config_parse__append__ret_t ret) { + switch (ret) { + case NM_CONFIG_PARSE__APPEND__RET_OK: + return NULL; + case NM_CONFIG_PARSE__APPEND__RET_ALLOC_ERROR: + return "error allocating memory"; + case NM_CONFIG_PARSE__APPEND__RET_ACTION_MUST_BE_AFTER_ITEM: + return "unexpected chain, must be directly after a menu item or another chain"; + default: + return "unknown error"; + } } -static void nm_config_push_generator(nm_config_t **cfg, nm_generator_t *it) { - nm_config_t *tmp = calloc(1, sizeof(nm_config_t)); - tmp->type = NM_CONFIG_TYPE_GENERATOR; - tmp->value.generator = it; - tmp->next = *cfg; - *cfg = tmp; -} +// note: line must point to the part after the config line type, and will be +// modified. if the config line type doesn't match, everything will be +// left as-is and false will be returned without an error. if an error +// occurs, err_out will be set and true will be returned (since stuff +// may be modified). otherwise, true is returned without an error (the +// parsed output will have strings pointing directly into the line). -static void nm_config_push_action(nm_menu_action_t **cur, nm_menu_action_t *act) { - if (*cur) - (*cur)->next = act; - *cur = act; -} +static bool nm_config_parse__lineend_action(int field, char **line, bool p_on_success, bool p_on_failure, nm_menu_action_t *act_out, char **err_out); +static bool nm_config_parse__line_item(const char *type, char **line, nm_menu_item_t *it_out, nm_menu_action_t *action_out, char **err_out); +static bool nm_config_parse__line_chain(const char *type, char **line, nm_menu_action_t *act_out, char **err_out); +static bool nm_config_parse__line_generator(const char *type, char **line, nm_generator_t *gn_out, char **err_out); -nm_config_t *nm_config_parse(char **err_out) { +nm_config_t *nm_config_parse(nm_config_file_t *files, char **err_out) { #define NM_ERR_RET NULL - NM_LOG("config: reading config dir %s", NM_CONFIG_DIR); - // set up the linked list - nm_config_t *cfg = NULL; + char *err = NULL; - // open the config dir - DIR *cfgdir; - NM_ASSERT((cfgdir = opendir(NM_CONFIG_DIR)), "could not open config dir: %s", strerror(errno)); + FILE *cfgfile = NULL; + char *line = NULL; + size_t line_bufsz = 0; + int line_n; + ssize_t line_sz; - // loop over the dirents of the config dir - struct dirent *dirent; errno = 0; - while ((dirent = readdir(cfgdir))) { - char *fn; - NM_ASSERT(asprintf(&fn, "%s/%s", NM_CONFIG_DIR, dirent->d_name) != -1, "could not build full path for config file"); + nm_config_parse__append__ret_t ret; + nm_config_parse__state_t state = {0}; - // skip it if it isn't a file - bool reg = dirent->d_type == DT_REG; - if (dirent->d_type == DT_UNKNOWN) { - struct stat statbuf; - NM_ASSERT(!stat(fn, &statbuf), "could not stat %s", fn); - reg = S_ISREG(statbuf.st_mode); - } - if (!reg) { - NM_LOG("config: skipping %s because not a regular file", fn); - continue; - } + nm_menu_item_t tmp_it; + nm_menu_action_t tmp_act; + nm_generator_t tmp_gn; - // skip special files, including: - // - dotfiles - // - vim: .*~, .*.s?? (unix only, usually swp or swo), *.swp, *.swo - // - gedit: .*~ - // - emacs: .*~, #*# - // - kate: .*.kate-swp - // - macOS: .DS_Store*, .Spotlight-V*, ._* - // - Windows: [Tt]humbs.db, desktop.ini - char *bn = dirent->d_name; - char *ex = strrchr(bn, '.'); - ex = (ex && ex != bn && bn[0] != '.') ? ex : NULL; - char lc = bn[strlen(bn)-1]; - if ((bn[0] == '.') || - (lc == '~') || - (bn[0] == '#' && lc == '#') || - (ex && (!strcmp(ex, ".swo") || !strcmp(ex, ".swp"))) || - (!strcmp(&bn[1], "humbs.db") && tolower(bn[0]) == 't') || - (!strcmp(bn, "desktop.ini"))) { - NM_LOG("config: skipping %s because it's a special file", fn); - continue; - } + #define RETERR(fmt, ...) do { \ + if (cfgfile) \ + fclose(cfgfile); \ + if (err_out) \ + asprintf(err_out, fmt, ##__VA_ARGS__); \ + free(err); \ + free(line); \ + nm_config_free(state.cfg_c); \ + return NM_ERR_RET; \ + } while (0) - // open the config file - NM_LOG("config: reading config file %s", fn); - FILE *cfgfile; - NM_ASSERT((cfgfile = fopen(fn, "r")), "could not open file: %s", strerror(errno)); + for (nm_config_file_t *cf = files; cf; cf = cf->next) { + NM_LOG("config: reading config file %s", cf->path); - #define RETERR(fmt, ...) do { \ - fclose(cfgfile); \ - free(line); \ - closedir(cfgdir); \ - NM_RETURN_ERR(fmt, ##__VA_ARGS__); \ - } while (0) + line_n = 0; + cfgfile = fopen(cf->path, "r"); - // parse each line - char *line; - int line_n = 0; - ssize_t line_sz; - size_t line_bufsz = 0; + if (!cfgfile) + RETERR("could not open file: %s", strerror(errno)); - nm_menu_item_t *it = NULL; - nm_menu_action_t *cur_act = NULL; while ((line_sz = getline(&line, &line_bufsz, cfgfile)) != -1) { line_n++; - // empty line or comment char *cur = strtrim(line); if (!*cur || *cur == '#') + continue; // empty line or comment + + char *s_typ = strtrim(strsep(&cur, ":")); + + if (nm_config_parse__line_item(s_typ, &cur, &tmp_it, &tmp_act, &err)) { + if (err) + RETERR("file %s: line %d: parse menu_item: %s", cf->path, line_n, err); + if ((ret = nm_config_parse__append_item(&state, &tmp_it))) + RETERR("file %s: line %d: error appending item to config: %s", cf->path, line_n, nm_config_parse__strerror(ret)); + if ((ret = nm_config_parse__append_action(&state, &tmp_act))) + RETERR("file %s: line %d: error appending action to config: %s", cf->path, line_n, nm_config_parse__strerror(ret)); continue; + } - // field 1: type - char *c_typ = strtrim(strsep(&cur, ":")); - if (!strcmp(c_typ, "menu_item")) { - // type: menu_item - if (it) nm_config_push_menu_item(&cfg, it); - it = calloc(1, sizeof(nm_menu_item_t)); - cur_act = NULL; + if (nm_config_parse__line_chain(s_typ, &cur, &tmp_act, &err)) { + if (err) + RETERR("file %s: line %d: parse chain: %s", cf->path, line_n, err); + if ((ret = nm_config_parse__append_action(&state, &tmp_act))) + RETERR("file %s: line %d: error appending action to config: %s", cf->path, line_n, nm_config_parse__strerror(ret)); + continue; + } - // type: menu_item - field 2: location - char *c_loc = strtrim(strsep(&cur, ":")); - if (!c_loc) RETERR("file %s: line %d: field 2: expected location, got end of line", fn, line_n); - else if (!strcmp(c_loc, "main")) it->loc = NM_MENU_LOCATION_MAIN_MENU; - else if (!strcmp(c_loc, "reader")) it->loc = NM_MENU_LOCATION_READER_MENU; - else RETERR("file %s: line %d: field 2: unknown location '%s'", fn, line_n, c_loc); + if (nm_config_parse__line_generator(s_typ, &cur, &tmp_gn, &err)) { + if (err) + RETERR("file %s: line %d: parse generator: %s", cf->path, line_n, err); + if ((ret = nm_config_parse__append_generator(&state, &tmp_gn))) + RETERR("file %s: line %d: error appending generator to config: %s", cf->path, line_n, nm_config_parse__strerror(ret)); + continue; + } - // type: menu_item - field 3: label - char *c_lbl = strtrim(strsep(&cur, ":")); - if (!c_lbl) RETERR("file %s: line %d: field 3: expected label, got end of line", fn, line_n); - else it->lbl = strdup(c_lbl); - - // type: menu_item - field 4: action - nm_menu_action_t *action = calloc(1, sizeof(nm_menu_action_t)); - action->on_failure = true; - action->on_success = true; - char *c_act = strtrim(strsep(&cur, ":")); - if (!c_act) RETERR("file %s: line %d: field 4: expected action, got end of line", fn, line_n); - #define X(name) else if (!strcmp(c_act, #name)) action->act = NM_ACTION(name); - NM_ACTIONS - #undef X - else RETERR("file %s: line %d: field 4: unknown action '%s'", fn, line_n, c_act); - - // type: menu_item - field 5: argument - char *c_arg = strtrim(cur); - if (!c_arg) RETERR("file %s: line %d: field 5: expected argument, got end of line\n", fn, line_n); - else action->arg = strdup(c_arg); - nm_config_push_action(&cur_act, action); - it->action = cur_act; - } else if (!strncmp(c_typ, "chain", 5)) { - // type: chain - if (!it) RETERR("file %s: line %d: unexpected chain, no menu_item to link to", fn, line_n); - nm_menu_action_t *action = calloc(1, sizeof(nm_menu_action_t)); - - if (!strcmp(c_typ, "chain")) { - RETERR("file %s: line %d: field 1: the chain action has been renamed to chain_success", fn, line_n); - } else if (!strcmp(c_typ, "chain_success")) { - action->on_failure = false; - action->on_success = true; - } else if (!strcmp(c_typ, "chain_always")) { - action->on_failure = true; - action->on_success = true; - } else if (!strcmp(c_typ, "chain_failure")) { - action->on_failure = true; - action->on_success = false; - } else RETERR("file %s: line %d: field 1: unknown type '%s'", fn, line_n, c_typ); - - // type: chain - field 2: action - char *c_act = strtrim(strsep(&cur, ":")); - if (!c_act) RETERR("file %s: line %d: field 2: expected action, got end of line", fn, line_n); - #define X(name) else if (!strcmp(c_act, #name)) action->act = NM_ACTION(name); - NM_ACTIONS - #undef X - else RETERR("file %s: line %d: field 2: unknown action '%s'", fn, line_n, c_act); - - // type: chain - field 3: argument - char *c_arg = strtrim(cur); - if (!c_arg) RETERR("file %s: line %d: field 3: expected argument, got end of line\n", fn, line_n); - else action->arg = strdup(c_arg); - nm_config_push_action(&cur_act, action); - } else if (!strcmp(c_typ, "generator")) { - // type: generator - nm_generator_fn_t generate; - nm_menu_location_t loc; - - // type: generator - field 2: location - char *c_loc = strtrim(strsep(&cur, ":")); - if (!c_loc) RETERR("file %s: line %d: field 2: expected location, got end of line", fn, line_n); - else if (!strcmp(c_loc, "main")) loc = NM_MENU_LOCATION_MAIN_MENU; - else if (!strcmp(c_loc, "reader")) loc = NM_MENU_LOCATION_READER_MENU; - else RETERR("file %s: line %d: field 2: unknown location '%s'", fn, line_n, c_loc); - - // type: generator - field 3: generator - char *c_gen = strtrim(strsep(&cur, ":")); - if (!c_gen) RETERR("file %s: line %d: field 3: expected generator, got end of line", fn, line_n); - #define X(name) else if (!strcmp(c_gen, #name)) generate = NM_GENERATOR(name); - NM_GENERATORS - #undef X - else RETERR("file %s: line %d: field 3: unknown generator '%s'", fn, line_n, c_gen); - - // type: generator - field 4: argument (optional) - char *c_arg = strtrim(cur); - - nm_generator_t *gen = calloc(1, sizeof(nm_generator_t)); - gen->desc = strdup(c_gen); - gen->loc = loc; - gen->arg = strdup(c_arg ? c_arg : ""); - gen->generate = generate; - nm_config_push_generator(&cfg, gen); - } else RETERR("file %s: line %d: field 1: unknown type '%s'", fn, line_n, c_typ); + RETERR("file %s: line %d: field 1: unknown type '%s'", cf->path, line_n, s_typ); } - // Push the last menu item onto the config - if (it) nm_config_push_menu_item(&cfg, it); - it = NULL; - cur_act = NULL; - #undef RETERR + + // reset the current per-file state + state.cfg_it_c = NULL; + state.cfg_it_act_s = NULL; + state.cfg_it_act_c = NULL; + state.cfg_gn_c = NULL; fclose(cfgfile); - free(line); + cfgfile = NULL; } - NM_ASSERT(!errno, "could not read config dir: %s", strerror(errno)); - // close the config dir - closedir(cfgdir); + if (!state.cfg_c) { + if ((ret = nm_config_parse__append_item(&state, &(nm_menu_item_t){ + .loc = NM_MENU_LOCATION_MAIN_MENU, + .lbl = "NickelMenu", + .action = NULL, + }))) RETERR("error appending default item to empty config: %s", nm_config_parse__strerror(ret)); - // add a default entry if none were found - if (!cfg) { - nm_menu_item_t *it = calloc(1, sizeof(nm_menu_item_t)); - nm_menu_action_t *action = calloc(1, sizeof(nm_menu_action_t)); - it->loc = NM_MENU_LOCATION_MAIN_MENU; - it->lbl = strdup("NickelMenu"); - it->action = action; - action->arg = strdup("See .adds/nm/doc for instructions on how to customize this menu."); - action->act = NM_ACTION(dbg_toast); - action->on_failure = true; - action->on_success = true; - nm_config_push_menu_item(&cfg, it); + if ((ret = nm_config_parse__append_action(&state, &(nm_menu_action_t){ + .act = NM_ACTION(dbg_toast), + .on_failure = true, + .on_success = true, + .arg = "See .adds/nm/doc for instructions on how to customize this menu.", + .next = NULL, + }))) RETERR("error appending default action to empty config: %s", nm_config_parse__strerror(ret)); } size_t mm = 0, rm = 0; - for (nm_config_t *cur = cfg; cur; cur = cur->next) { + for (nm_config_t *cur = state.cfg_s; cur; cur = cur->next) { switch (cur->type) { case NM_CONFIG_TYPE_MENU_ITEM: NM_LOG("cfg(NM_CONFIG_TYPE_MENU_ITEM) : %d:%s", cur->value.menu_item->loc, cur->value.menu_item->lbl); @@ -275,14 +335,250 @@ nm_config_t *nm_config_parse(char **err_out) { break; } } - NM_ASSERT(mm <= NM_CONFIG_MAX_MENU_ITEMS_PER_MENU, "too many menu items in main menu (> %d)", NM_CONFIG_MAX_MENU_ITEMS_PER_MENU); - NM_ASSERT(rm <= NM_CONFIG_MAX_MENU_ITEMS_PER_MENU, "too many menu items in reader menu (> %d)", NM_CONFIG_MAX_MENU_ITEMS_PER_MENU); - // return the head of the list - NM_RETURN_OK(cfg); + if (mm > NM_CONFIG_MAX_MENU_ITEMS_PER_MENU) + RETERR("too many menu items in main menu (> %d)", NM_CONFIG_MAX_MENU_ITEMS_PER_MENU); + if (rm > NM_CONFIG_MAX_MENU_ITEMS_PER_MENU) + RETERR("too many menu items in reader menu (> %d)", NM_CONFIG_MAX_MENU_ITEMS_PER_MENU); + + NM_RETURN_OK(state.cfg_s); #undef NM_ERR_RET } +static bool nm_config_parse__line_item(const char *type, char **line, nm_menu_item_t *it_out, nm_menu_action_t *action_out, char **err_out) { + #define NM_ERR_RET true + + if (strcmp(type, "menu_item")) + NM_RETURN_OK(false); + + *it_out = (nm_menu_item_t){0}; + + char *s_loc = strtrim(strsep(line, ":")); + if (!s_loc) NM_RETURN_ERR("field 2: expected location, got end of line"); + else if (!strcmp(s_loc, "main")) it_out->loc = NM_MENU_LOCATION_MAIN_MENU; + else if (!strcmp(s_loc, "reader")) it_out->loc = NM_MENU_LOCATION_READER_MENU; + else NM_RETURN_ERR("field 2: unknown location '%s'", s_loc); + + char *p_lbl = strtrim(strsep(line, ":")); + if (!p_lbl) NM_RETURN_ERR("field 3: expected label, got end of line"); + it_out->lbl = p_lbl; + + nm_config_parse__lineend_action(4, line, true, true, action_out, err_out); + if (*err_out) + return NM_ERR_RET; + + NM_RETURN_OK(true); + #undef NM_ERR_RET +} + +static bool nm_config_parse__line_chain(const char *type, char **line, nm_menu_action_t *act_out, char **err_out) { + #define NM_ERR_RET true + + if (strncmp(type, "chain_", 5)) + NM_RETURN_OK(false); + + bool p_on_success, p_on_failure; + if (!strcmp(type, "chain_success")) { + p_on_success = true; + p_on_failure = false; + } else if (!strcmp(type, "chain_always")) { + p_on_success = true; + p_on_failure = true; + } else if (!strcmp(type, "chain_failure")) { + p_on_success = false; + p_on_failure = true; + } else NM_RETURN_OK(false); + + nm_config_parse__lineend_action(2, line, p_on_success, p_on_failure, act_out, err_out); + if (*err_out) + return NM_ERR_RET; + + NM_RETURN_OK(true); + #undef NM_ERR_RET +} + +static bool nm_config_parse__line_generator(const char *type, char **line, nm_generator_t *gn_out, char **err_out) { + #define NM_ERR_RET true + + if (strcmp(type, "generator")) + NM_RETURN_OK(false); + + *gn_out = (nm_generator_t){0}; + + char *s_loc = strtrim(strsep(line, ":")); + if (!s_loc) NM_RETURN_ERR("field 2: expected location, got end of line"); + else if (!strcmp(s_loc, "main")) gn_out->loc = NM_MENU_LOCATION_MAIN_MENU; + else if (!strcmp(s_loc, "reader")) gn_out->loc = NM_MENU_LOCATION_READER_MENU; + else NM_RETURN_ERR("field 2: unknown location '%s'", s_loc); + + char *s_generate = strtrim(strsep(line, ":")); + if (!s_generate) NM_RETURN_ERR("field 3: expected generator, got end of line"); + #define X(name) \ + else if (!strcmp(s_generate, #name)) gn_out->generate = NM_GENERATOR(name); + NM_GENERATORS + #undef X + else NM_RETURN_ERR("field 3: unknown generator '%s'", s_generate); + + char *p_arg = strtrim(*line); // note: optional + if (p_arg) gn_out->arg = p_arg; + + gn_out->desc = s_generate; + + NM_RETURN_OK(true); + #undef NM_ERR_RET +} + +static bool nm_config_parse__lineend_action(int field, char **line, bool p_on_success, bool p_on_failure, nm_menu_action_t *act_out, char **err_out) { + #define NM_ERR_RET true + + *act_out = (nm_menu_action_t){0}; + + char *s_act = strtrim(strsep(line, ":")); + if (!s_act) NM_RETURN_ERR("field %d: expected action, got end of line", field); + #define X(name) \ + else if (!strcmp(s_act, #name)) act_out->act = NM_ACTION(name); + NM_ACTIONS + #undef X + else NM_RETURN_ERR("field %d: unknown action '%s'", field, s_act); + + // type: menu_item - field 5: argument + char *p_arg = strtrim(*line); + if (!p_arg) NM_RETURN_ERR("field %d: expected argument, got end of line\n", field+1); + act_out->arg = p_arg; + + act_out->on_success = p_on_success; + act_out->on_failure = p_on_failure; + + NM_RETURN_OK(true); + #undef NM_ERR_RET +} + +static nm_config_parse__append__ret_t nm_config_parse__append_item(nm_config_parse__state_t *restrict state, nm_menu_item_t *const restrict it) { + nm_config_t *cfg_n = calloc(1, sizeof(nm_config_t)); + nm_menu_item_t *cfg_it_n = calloc(1, sizeof(nm_menu_item_t)); + + if (!cfg_n || !cfg_it_n) { + free(cfg_n); + free(cfg_it_n); + return NM_CONFIG_PARSE__APPEND__RET_ALLOC_ERROR; + } + + *cfg_n = (nm_config_t){ + .type = NM_CONFIG_TYPE_MENU_ITEM, + .generated = false, + .value = { .menu_item = cfg_it_n }, + .next = NULL, + }; + + *cfg_it_n = (nm_menu_item_t){ + .loc = it->loc, + .lbl = strdup(it->lbl ? it->lbl : ""), + .action = NULL, + }; + + if (!cfg_it_n->lbl) { + free(cfg_n); + free(cfg_it_n); + return NM_CONFIG_PARSE__APPEND__RET_ALLOC_ERROR; + } + + if (state->cfg_c) + state->cfg_c->next = cfg_n; + else + state->cfg_s = cfg_n; + + state->cfg_c = cfg_n; + state->cfg_it_c = cfg_it_n; + + state->cfg_it_act_s = NULL; + state->cfg_it_act_c = NULL; + state->cfg_gn_c = NULL; + + return NM_CONFIG_PARSE__APPEND__RET_OK; +} + +static nm_config_parse__append__ret_t nm_config_parse__append_action(nm_config_parse__state_t *restrict state, nm_menu_action_t *const restrict act) { + if (!state->cfg_c || !state->cfg_it_c || state->cfg_gn_c) + return NM_CONFIG_PARSE__APPEND__RET_ACTION_MUST_BE_AFTER_ITEM; + + nm_menu_action_t *cfg_it_act_n = calloc(1, sizeof(nm_menu_action_t)); + + if (!cfg_it_act_n) + return NM_CONFIG_PARSE__APPEND__RET_ALLOC_ERROR; + + *cfg_it_act_n = (nm_menu_action_t){ + .act = act->act, + .on_failure = act->on_failure, + .on_success = act->on_success, + .arg = strdup(act->arg ? act->arg : ""), + .next = NULL, + }; + + if (!cfg_it_act_n->arg) { + free(cfg_it_act_n); + return NM_CONFIG_PARSE__APPEND__RET_ALLOC_ERROR; + } + + if (!state->cfg_it_c->action) + state->cfg_it_c->action = cfg_it_act_n; + + if (state->cfg_it_act_c) + state->cfg_it_act_c->next = cfg_it_act_n; + else + state->cfg_it_act_s = cfg_it_act_n; + + state->cfg_it_act_c = cfg_it_act_n; + + return NM_CONFIG_PARSE__APPEND__RET_OK; +} + +static nm_config_parse__append__ret_t nm_config_parse__append_generator(nm_config_parse__state_t *restrict state, nm_generator_t *const restrict gn) { + nm_config_t *cfg_n = calloc(1, sizeof(nm_config_t)); + nm_generator_t *cfg_gn_n = calloc(1, sizeof(nm_generator_t)); + + if (!cfg_n || !cfg_gn_n) { + free(cfg_n); + free(cfg_gn_n); + return NM_CONFIG_PARSE__APPEND__RET_ALLOC_ERROR; + } + + *cfg_n = (nm_config_t){ + .type = NM_CONFIG_TYPE_GENERATOR, + .generated = false, + .value = { .generator = cfg_gn_n }, + .next = NULL, + }; + + *cfg_gn_n = (nm_generator_t){ + .desc = strdup(gn->desc ? gn->desc : ""), + .loc = gn->loc, + .arg = strdup(gn->arg ? gn->arg : ""), + .generate = gn->generate, + }; + + if (!cfg_gn_n->desc || !cfg_gn_n->arg) { + free(cfg_gn_n->desc); + free(cfg_gn_n->arg); + free(cfg_n); + free(cfg_gn_n); + return NM_CONFIG_PARSE__APPEND__RET_ALLOC_ERROR; + } + + if (state->cfg_c) + state->cfg_c->next = cfg_n; + else + state->cfg_s = cfg_n; + + state->cfg_c = cfg_n; + state->cfg_gn_c = cfg_gn_n; + + state->cfg_it_c = NULL; + state->cfg_it_act_s = NULL; + state->cfg_it_act_c = NULL; + + return NM_CONFIG_PARSE__APPEND__RET_OK; +} + void nm_config_generate(nm_config_t *cfg) { NM_LOG("config: removing any previously generated items"); for (nm_config_t *prev = NULL, *cur = cfg; cur; cur = cur->next) { @@ -309,7 +605,7 @@ void nm_config_generate(nm_config_t *cfg) { } NM_LOG("config: ... %zu items generated", sz); - for (size_t i = 0; i < sz; i++) { + for (ssize_t i = sz-1; i >= 0; i--) { nm_config_t *tmp = calloc(1, sizeof(nm_config_t)); tmp->type = NM_CONFIG_TYPE_MENU_ITEM; tmp->value.menu_item = items[i]; @@ -348,10 +644,10 @@ nm_menu_item_t **nm_config_get_menu(nm_config_t *cfg, size_t *n_out) { if (!it) return NULL; - nm_menu_item_t **tmp = &it[*n_out - 1]; + nm_menu_item_t **tmp = it; for (nm_config_t *cur = cfg; cur; cur = cur->next) if (cur->type == NM_CONFIG_TYPE_MENU_ITEM) - *(tmp--) = cur->value.menu_item; + *(tmp++) = cur->value.menu_item; return it; } diff --git a/src/config.h b/src/config.h index 864bd8f..b8f596d 100644 --- a/src/config.h +++ b/src/config.h @@ -17,10 +17,26 @@ extern "C" { typedef struct nm_config_t nm_config_t; -// nm_config_parse parses the configuration files in /mnt/onboard/.adds/nm. -// An error is returned if there are syntax errors, file access errors, or -// invalid action names for menu_item. -nm_config_t *nm_config_parse(char **err_out); +typedef struct nm_config_file_t nm_config_file_t; + +// nm_config_parse lists the configuration files in /mnt/onboard/.adds/nm. An +// error is returned if there are errors reading the dir. +nm_config_file_t *nm_config_files(char **err_out); + +// nm_config_files_update checks if the configuration files are up to date and +// updates them, returning true, if not. If *files is NULL, it is equivalent to +// doing `*files = nm_config_files(err_out)`. If an error occurs, the pointer is +// left untouched and false is returned along with the error. Warning: if the +// files have changed, the pointer passed to files will become invalid (it gets +// replaced). +bool nm_config_files_update(nm_config_file_t **files, char **err_out); + +// nm_config_files_free frees the list of configuration files. +void nm_config_files_free(nm_config_file_t *files); + +// nm_config_parse parses the configuration files. An error is returned if there +// are syntax errors, file access errors, or invalid action names for menu_item. +nm_config_t *nm_config_parse(nm_config_file_t *files, char **err_out); // nm_config_generate runs all generators synchronously and sequentially. Any // previously generated items are automatically removed. diff --git a/src/init.c b/src/init.c index 2cd71cd..31f1291 100644 --- a/src/init.c +++ b/src/init.c @@ -10,6 +10,7 @@ #include "action.h" #include "config.h" #include "failsafe.h" +#include "init.h" #include "menu.h" #include "util.h" @@ -30,6 +31,7 @@ __attribute__((constructor)) void nm_init() { NM_LOG("config dir: %s", NM_CONFIG_DIR); NM_LOG("init: creating failsafe"); + nm_failsafe_t *fs; if (!(fs = nm_failsafe_create(&err)) && err) { NM_LOG("error: could not create failsafe: %s, stopping", err); @@ -38,6 +40,7 @@ __attribute__((constructor)) void nm_init() { } NM_LOG("init: checking for uninstall flag"); + if (!access(NM_CONFIG_DIR "/uninstall", F_OK)) { NM_LOG("init: flag found, uninstalling"); nm_failsafe_uninstall(fs); @@ -47,6 +50,7 @@ __attribute__((constructor)) void nm_init() { #ifdef NM_UNINSTALL_CONFIGDIR NM_LOG("init: NM_UNINSTALL_CONFIGDIR: checking if config dir exists"); + if (access(NM_CONFIG_DIR, F_OK) && errno == ENOENT) { NM_LOG("init: config dir does not exist, uninstalling"); nm_failsafe_uninstall(fs); @@ -54,37 +58,27 @@ __attribute__((constructor)) void nm_init() { } #endif - NM_LOG("init: parsing config"); - size_t items_n; - nm_menu_item_t **items; - nm_config_t *cfg; - if (!(cfg = nm_config_parse(&err)) && err) { - NM_LOG("error: could not parse config: %s, creating error item in main menu instead", err); + NM_LOG("init: updating config"); - items_n = 1; - items = calloc(items_n, sizeof(nm_menu_item_t*)); - items[0] = calloc(1, sizeof(nm_menu_item_t)); - items[0]->loc = NM_MENU_LOCATION_MAIN_MENU; - items[0]->lbl = strdup("Config Error"); - items[0]->action = calloc(1, sizeof(nm_menu_action_t)); - items[0]->action->arg = strdup(err); - items[0]->action->act = NM_ACTION(dbg_msg); - items[0]->action->on_failure = true; - items[0]->action->on_success = true; + bool upd = nm_global_config_update(&err); + if (err) { + NM_LOG("init: error parsing config, will show a menu item with the error: %s", err); + } - free(err); - } else { - NM_LOG("init: generating items"); - nm_config_generate(cfg); - - NM_LOG("init: getting menu"); - if (!(items = nm_config_get_menu(cfg, &items_n))) { - NM_LOG("error: could not allocate memory, stopping"); - goto stop_fs; - } + size_t ntmp = SIZE_MAX; + if (!upd) { + NM_LOG("init: no config file changes detected for initial config update (it should always return an error or update), stopping (this is a bug; err should have been returned instead)"); + return; + } else if (!nm_global_config_items(&ntmp)) { + NM_LOG("init: warning: no menu items returned by nm_global_config_items, ignoring for now (this is a bug; it should always have a menu item whether the default, an error, or the actual config)"); + } else if (ntmp == SIZE_MAX) { + NM_LOG("init: warning: no size returned by nm_global_config_items, ignoring for now (this is a bug)"); + } else if (!ntmp) { + NM_LOG("init: warning: size returned by nm_global_config_items is 0, ignoring for now (this is a bug; it should always have a menu item whether the default, an error, or the actual config)"); } NM_LOG("init: opening libnickel"); + void *libnickel = dlopen("libnickel.so.1.0.0", RTLD_LAZY|RTLD_NODELETE); if (!libnickel) { NM_LOG("error: could not dlopen libnickel, stopping"); @@ -92,7 +86,8 @@ __attribute__((constructor)) void nm_init() { } NM_LOG("init: hooking libnickel"); - if (nm_menu_hook(libnickel, items, items_n, &err) && err) { + + if (nm_menu_hook(libnickel, &err) && err) { NM_LOG("error: could not hook libnickel: %s, stopping", err); free(err); goto stop_fs; @@ -106,3 +101,95 @@ stop: NM_LOG("init: done"); return; } + +// note: not thread safe +static nm_config_file_t *nm_global_menu_config_files = NULL; // updated in-place by nm_global_config_update +static nm_config_t *nm_global_menu_config = NULL; // updated by nm_global_config_update, replaced by nm_global_config_replace, NULL on error +static nm_menu_item_t **nm_global_menu_config_items = NULL; // updated by nm_global_config_replace to an error message or the items from nm_global_menu_config +static size_t nm_global_menu_config_n = 0; // ^ + +nm_menu_item_t **nm_global_config_items(size_t *n_out) { + if (n_out) + *n_out = nm_global_menu_config_n; + return nm_global_menu_config_items; +} + +static void nm_global_config_replace(nm_config_t *cfg, const char *err) { + if (nm_global_menu_config_n) + nm_global_menu_config_n = 0; + + if (nm_global_menu_config_items) { + free(nm_global_menu_config_items); + nm_global_menu_config_items = NULL; + } + + if (nm_global_menu_config) { + nm_config_free(nm_global_menu_config); + nm_global_menu_config = NULL; + } + + if (err && cfg) + nm_config_free(cfg); + + // this isn't strictly necessary, but we should always try to reparse it + // every time just in case the error was temporary + if (err && nm_global_menu_config_files) { + nm_config_files_free(nm_global_menu_config_files); + nm_global_menu_config_files = NULL; + } + + if (err) { + nm_global_menu_config_n = 1; + nm_global_menu_config_items = calloc(nm_global_menu_config_n, sizeof(nm_menu_item_t*)); + nm_global_menu_config_items[0] = calloc(1, sizeof(nm_menu_item_t)); + nm_global_menu_config_items[0]->loc = NM_MENU_LOCATION_MAIN_MENU; + nm_global_menu_config_items[0]->lbl = strdup("Config Error"); + nm_global_menu_config_items[0]->action = calloc(1, sizeof(nm_menu_action_t)); + nm_global_menu_config_items[0]->action->arg = strdup(err); + nm_global_menu_config_items[0]->action->act = NM_ACTION(dbg_msg); + nm_global_menu_config_items[0]->action->on_failure = true; + nm_global_menu_config_items[0]->action->on_success = true; + return; + } + + nm_global_menu_config_items = nm_config_get_menu(cfg, &nm_global_menu_config_n); + if (!nm_global_menu_config_items) + NM_LOG("could not allocate memory"); +} + +bool nm_global_config_update(char **err_out) { + #define NM_ERR_RET true + char *err; + + NM_LOG("global: scanning for config files"); + bool updated = nm_config_files_update(&nm_global_menu_config_files, &err); + if (err) { + NM_LOG("... error: %s", err); + NM_LOG("global: freeing old config and replacing with error item"); + nm_global_config_replace(NULL, err); + NM_RETURN_ERR("scan for config files: %s", err); + } + + NM_LOG("global:%s changes detected", updated ? "" : " no"); + if (!updated) + NM_RETURN_OK(false); + + NM_LOG("global: parsing new config"); + nm_config_t *cfg = nm_config_parse(nm_global_menu_config_files, &err); + if (err) { + NM_LOG("... error: %s", err); + NM_LOG("global: freeing old config and replacing with error item"); + nm_global_config_replace(NULL, err); + NM_RETURN_ERR("parse config files: %s", err); + } + + NM_LOG("global: running generators"); + nm_config_generate(cfg); + + NM_LOG("global: freeing old config and replacing with new one"); + nm_global_config_replace(cfg, NULL); + + NM_LOG("global: done swapping config"); + NM_RETURN_OK(true); + #undef NM_ERR_RET +} diff --git a/src/init.h b/src/init.h new file mode 100644 index 0000000..15916da --- /dev/null +++ b/src/init.h @@ -0,0 +1,28 @@ +#ifndef NM_INIT_H +#define NM_INIT_H +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include "menu.h" + +// nm_global_config_update updates and regenerates the config if needed, and +// returns true if it needed updating. If an error occurs, true is also returned +// since the menu items will be updated to a single one showing the error (see +// nm_global_config_items). +bool nm_global_config_update(char **err_out); + +// nm_global_config_items returns an array of pointers with the current menu +// items (the pointer and the items it points to will remain valid until the +// next time nm_global_config_update is called). The number of items is stored +// in the variable pointed to by n_out. If an error ocurred during the last time +// nm_global_config_update was called, it is returned as a "Config Error" menu +// item. If nm_global_config_update has never been called successfully before, +// NULL is returned and n_out is set to 0. +nm_menu_item_t **nm_global_config_items(size_t *n_out); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/menu.cc b/src/menu.cc index 914633f..253c040 100644 --- a/src/menu.cc +++ b/src/menu.cc @@ -7,8 +7,8 @@ #include #include -#include "action.h" #include "dlhook.h" +#include "init.h" #include "menu.h" #include "util.h" @@ -60,10 +60,7 @@ void (*MainWindowController_toast)(MainWindowController*, QString const&, QStrin static void (*LightMenuSeparator_LightMenuSeparator)(void*, QWidget*); static void (*BoldMenuSeparator_BoldMenuSeparator)(void*, QWidget*); -static nm_menu_item_t **_items; -static size_t _items_n; - -extern "C" int nm_menu_hook(void *libnickel, nm_menu_item_t **items, size_t items_n, char **err_out) { +extern "C" int nm_menu_hook(void *libnickel, char **err_out) { #define NM_ERR_RET 1 //libnickel 4.6 * _ZN28AbstractNickelMenuController18createMenuTextItemEP5QMenuRK7QStringbbS4_ reinterpret_cast(AbstractNickelMenuController_createMenuTextItem) = dlsym(libnickel, "_ZN28AbstractNickelMenuController18createMenuTextItemEP5QMenuRK7QStringbbS4_"); @@ -98,9 +95,6 @@ extern "C" int nm_menu_hook(void *libnickel, nm_menu_item_t **items, size_t item reinterpret_cast(AbstractNickelMenuController_createMenuTextItem_orig) = nm_dlhook(libnickel, "_ZN28AbstractNickelMenuController18createMenuTextItemEP5QMenuRK7QStringbbS4_", nmh, &err); NM_ASSERT(AbstractNickelMenuController_createMenuTextItem_orig, "failed to hook _ZN28AbstractNickelMenuController18createMenuTextItemEP5QMenuRK7QStringbbS4_: %s", err); - _items = items; - _items_n = items_n; - NM_RETURN_OK(0); #undef NM_ERR_RET } @@ -108,7 +102,8 @@ extern "C" int nm_menu_hook(void *libnickel, nm_menu_item_t **items, size_t item // AbstractNickelMenuController_createAction_before wraps // AbstractNickelMenuController::createAction to use the correct separator for // the menu location and to match the behaviour of QMenu::insertAction instead -// of QMenu::addAction. +// of QMenu::addAction. It also adds the property nm_action=true to the action +// and separator. QAction *AbstractNickelMenuController_createAction_before(QAction *before, nm_menu_location_t loc, bool last_in_group, void *_this, QMenu *menu, QWidget *widget, bool close, bool enabled, bool separator); // nm_menu_item_do runs a nm_menu_item_t and must be called from the thread of a @@ -143,6 +138,21 @@ extern "C" MenuTextItem* _nm_menu_hook(void* _this, QMenu* menu, QString const& void _nm_menu_inject(void *nmc, QMenu *menu, nm_menu_location_t loc, int at) { NM_LOG("inject %d @ %d", loc, at); + NM_LOG("checking for config updates"); + bool updated = nm_global_config_update(NULL); // if there was an error it will be returned as a menu item anyways (and updated will be true) + NM_LOG("updated = %d", updated); + + NM_LOG("checking for existing items added by nm"); + + for (auto action : menu->actions()) { + if (action->property("nm_action") == true) { + if (!updated) + return; // already added items, menu is up to date + menu->removeAction(action); + delete action; + } + } + NM_LOG("getting insertion point"); auto actions = menu->actions(); @@ -153,34 +163,31 @@ void _nm_menu_inject(void *nmc, QMenu *menu, nm_menu_location_t loc, int at) { if (before == nullptr) NM_LOG("it seems the original item to add new ones before was never actually added to the menu (number of items when the action was created is %d, current is %d), appending to end instead", at, actions.count()); - NM_LOG("checking for old items"); - - for (auto action : actions) { - if (action->property("nm_action") == true) { - return; // already added - /*menu->removeAction(action); - delete action;*/ - } - } - NM_LOG("injecting new items"); + size_t items_n; + nm_menu_item_t **items = nm_global_config_items(&items_n); + + if (!items) { + NM_LOG("items is NULL (either the config hasn't been parsed yet or there was a memory allocation error), not adding"); + return; + } + // if it segfaults in createMenuTextItem, it's likely because // AbstractNickelMenuController is invalid, which shouldn't happen while the // menu which we added the signal from still can be shown... (but // theoretically, it's possible) - for (size_t i = 0; i < _items_n; i++) { - nm_menu_item_t *it = _items[i]; + for (size_t i = 0; i < items_n; i++) { + nm_menu_item_t *it = items[i]; if (it->loc != loc) continue; NM_LOG("adding items '%s'...", it->lbl); MenuTextItem* item = AbstractNickelMenuController_createMenuTextItem_orig(nmc, menu, QString::fromUtf8(it->lbl), false, false, ""); - QAction* action = AbstractNickelMenuController_createAction_before(before, loc, i == _items_n-1, nmc, menu, item, true, true, true); + QAction* action = AbstractNickelMenuController_createAction_before(before, loc, i == items_n-1, nmc, menu, item, true, true, true); - action->setProperty("nm_action", true); QObject::connect(action, &QAction::triggered, [it](bool){ NM_LOG("item '%s' pressed...", it->lbl); nm_menu_item_do(it); @@ -262,6 +269,8 @@ QAction *AbstractNickelMenuController_createAction_before(QAction *before, nm_me int n = menu->actions().count(); QAction* action = AbstractNickelMenuController_createAction(_this, menu, widget, /*close*/false, enabled, /*separator*/false); + action->setProperty("nm_action", true); + if (!menu->actions().contains(action)) { NM_LOG("could not find added action at end of menu (note: old count is %d, new is %d), not moving it to the right spot or adding separator", n, menu->actions().count()); return action; @@ -279,16 +288,18 @@ QAction *AbstractNickelMenuController_createAction_before(QAction *before, nm_me if (separator) { // if it's the main menu, we generally want to use a custom separator + QAction *sep; if (loc == NM_MENU_LOCATION_MAIN_MENU && LightMenuSeparator_LightMenuSeparator && BoldMenuSeparator_BoldMenuSeparator) { - QAction *lsp = reinterpret_cast(calloc(1, 32)); // it's actually 8 as of 14622, but better to be safe + sep = reinterpret_cast(calloc(1, 32)); // it's actually 8 as of 14622, but better to be safe (last_in_group ? BoldMenuSeparator_BoldMenuSeparator : LightMenuSeparator_LightMenuSeparator - )(lsp, reinterpret_cast(_this)); - menu->insertAction(before, lsp); + )(sep, reinterpret_cast(_this)); + menu->insertAction(before, sep); } else { - menu->insertSeparator(before); + sep = menu->insertSeparator(before); } + sep->setProperty("nm_action", true); } return action; diff --git a/src/menu.h b/src/menu.h index 68c30ce..947b4fc 100644 --- a/src/menu.h +++ b/src/menu.h @@ -28,12 +28,9 @@ typedef struct { nm_menu_action_t *action; } nm_menu_item_t; -// nm_menu_hook hooks a dlopen'd libnickel handle to add the specified menus, -// and returns 0 on success, or 1 with err_out (if provided) set to the malloc'd -// error message on error. The provided configuration and all pointers it -// references must remain valid for the lifetime of the program (i.e. not stack -// allocated). It MUST NOT be called more than once. -int nm_menu_hook(void *libnickel, nm_menu_item_t **items, size_t items_n, char **err_out); +// nm_menu_hook hooks a dlopen'd libnickel handle. It MUST NOT be called more +// than once. +int nm_menu_hook(void *libnickel, char **err_out); #ifdef __cplusplus }