From 4c4ae412ddd1507848fe0cc45045425f01669580 Mon Sep 17 00:00:00 2001 From: Charalampos Kardaris Date: Tue, 3 Dec 2019 15:25:54 +0200 Subject: Added missing popup menu functionality The existing code had this functionality planned but not implemented. I tried my best to make it work and I updated the README file with instructions on how to use it. --- README.md | 9 ++++ mktrayicon.c | 139 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 145 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f5e97d6..d4fe5bb 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,9 @@ are supported: - `c `: Set the command to be execute when the user clicks the icon (`cmnd` is passed to `/bin/sh -c`) - `c`: Remove the click handler + - `m ,|,|...`: Set the labels and the corresponding commands to be executed when the user opens the icon menu (right-click usually) + (`cmd#` is passed to `/bin/sh -c`) + - `m`: Remove the menu handler - `h`: Hide the tray icon - `s`: Show the tray icon @@ -39,6 +42,12 @@ Quoted strings are terminated by a matching quote at the end of a line (ignoring whitespace). To escape a quote character at the end of a line to continue a quoted string, prefix it with a `\`. +The m(enu) command uses `,` as a delimiter between label and command and `|` as a delimiter +between entries (label+command). If you want to use these 2 characters in a label or command, you have to escape +them with `\`. + +Example command: `echo "m Browser,firefox|Terminal,xterm" > /tmp/test` (where `mkfifo /tmp/test` has been executed before) + ## Why? Because I wanted to be able to create tray icons from bash without all the diff --git a/mktrayicon.c b/mktrayicon.c index 6f063a0..9528495 100644 --- a/mktrayicon.c +++ b/mktrayicon.c @@ -13,6 +13,36 @@ #include #include +/* + * This function is made because it's needed to escape the '\' character + * The basic code has been taken from the man pages of the strncpy function + */ +char* strncpy_esc(char *dest, const char *src, size_t n) +{ + size_t i; + size_t index = 0; + for (i = 0; i < n && src[i] != '\0'; i++){ + if (src[i] != '\\'){ + dest[index] = src[i]; + index++; + } + } + for ( ; index < n; index++) + dest[index] = '\0'; + return dest; +} + +/* + * Struct that stores the label names on the menu and + * their corresponding actions when the user selects them + */ +struct item{ + char* name; + char* action; +} *onmenu; +int menusize = 0; //number of menu entries +GtkWidget* menu = NULL; + GtkStatusIcon *icon; char *onclick = NULL; @@ -24,12 +54,26 @@ void tray_icon_on_click(GtkStatusIcon *status_icon, } } +/* + * Callback function for when an entry is selected from the menu + * We loop over all entry names to find what action to execute + */ +void click_menu_item(GtkMenuItem *menuitem, gpointer user_data) +{ + const char* label = gtk_menu_item_get_label(menuitem); + for (int i = 0; i < menusize; i++) + if(strcmp(label,onmenu[i].name) == 0 && fork() == 0) + execl("/bin/sh", "sh", "-c", onmenu[i].action, (char *) NULL); +} + void tray_icon_on_menu(GtkStatusIcon *status_icon, guint button, guint activate_time, gpointer user_data) { #ifdef DEBUG printf("Popup menu\n"); #endif + if (menusize) + gtk_menu_popup_at_pointer((GtkMenu *)menu, NULL); } gboolean set_tooltip(gpointer data) @@ -256,10 +300,99 @@ gpointer watch_fifo(gpointer argv) #endif break; case 'm': /* menu */ - fprintf(stderr, "Menu command not yet implemented\n"); - if (param != NULL) { - free(param); + if (onmenu != NULL){ + //destroy the previous menu + for (int i = 0; i < menusize; i++){ + free(onmenu[i].name); + free(onmenu[i].action); + onmenu[i].name = NULL; + onmenu[i].action = NULL; + } + free(onmenu); + onmenu = NULL; + gtk_widget_destroy(menu); + menu = NULL; } + + menusize = 0; + + if (param != NULL && *param == '\0') { +#ifdef DEBUG + printf("Removing onmenu handler\n"); +#endif + break; + } + + // This block makes sure that the parameter after 'm' is well defined + int commas = 0; + int bars = 0; + if (len < 3){ + break; + } + if (param[0] == ',' || param[0] == '|'){ + printf("Action string cannot begin with ',' or '|'. Use '\\' to escape\n"); + break; + } + if (param[len-2] == '|' && param[len-3] != '\\'){ + printf("'|' can't be second of last. Use '\\' to escape\n"); + break; + } + if (param[len-1] == ',' && param[len-2] != '\\'){ + printf("',' can't be last. Use '\\' to escape\n"); + break; + } + int i = 1; + while (i < len - 1){ + if (commas > bars + 1 || bars > commas) + break; + if (param[i] == ',' && param[i-1] != '\\') + commas++; + else if (param[i] == '|' && param[i-1] != '\\') + bars++; + i++; + } + if (commas > bars + 1 || bars > commas){ + printf("Unbalanced ',' or '|'. Use '\\' to escape\n"); + break; + } + // End of block that checks the parameter + + // Create the onmenu array which stores structs with name, action properties + menusize = commas; + onmenu = malloc(menusize * sizeof(struct item)); + menu = gtk_menu_new(); + int last = -1; + int item = 0; + i = 1; + while(item < menusize && param[i] != '\0'){ + if (param[i] == ',' && param[i-1] != '\\'){ + onmenu[item].name = malloc((i-last) * sizeof(char)); + strncpy_esc(onmenu[item].name, param+last+1, i-last-1); + onmenu[item].name[i-last-1] = '\0'; + last = i; + } + else if (param[i] == '|' && param[i-1] != '\\'){ + onmenu[item].action = malloc((i-last) * sizeof(char)); + strncpy_esc(onmenu[item].action, param+last+1, i-last-1); + onmenu[item].action[i-last-1] = '\0'; + last = i; + item++; + } + i++; + } + if(item < menusize){ //haven't read all actions because last one didn't end with a '|' + onmenu[item].action = malloc((len-last) * sizeof(char)); + strncpy_esc(onmenu[item].action, param+last+1, len-last-1); + onmenu[item].action[len-last-1] = '\0'; + } + // Now create the menu item widgets and attach them on the menu + for (int i = 0; i < menusize; i++){ + GtkWidget* w = gtk_menu_item_new_with_label(onmenu[i].name); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), w); + g_signal_connect(G_OBJECT(w), "activate", G_CALLBACK(click_menu_item), NULL); + } + gtk_widget_show_all(menu); + break; default: fprintf(stderr, "Unknown command: '%c'\n", *buf); -- cgit From 184e8e48d9622c3664eab8b56d7478fc850c6b1e Mon Sep 17 00:00:00 2001 From: Charalampos Kardaris Date: Tue, 3 Dec 2019 23:08:28 +0200 Subject: Fixed accepted format of parameter for command 'm' Added free() commands where appropriate Made the logic a bit clearer and resistant to weird inputs --- mktrayicon.c | 101 ++++++++++++++++++++++++++++++++++++----------------------- 1 file changed, 61 insertions(+), 40 deletions(-) diff --git a/mktrayicon.c b/mktrayicon.c index 9528495..c1eb98d 100644 --- a/mktrayicon.c +++ b/mktrayicon.c @@ -32,6 +32,13 @@ char* strncpy_esc(char *dest, const char *src, size_t n) return dest; } +char* save_word(char* src, int i_del, int last){ + char* dest = malloc((i_del-last) * sizeof(char)); + strncpy_esc(dest, src+last+1, i_del-last-1); + dest[i_del-last-1] = '\0'; + return dest; +} + /* * Struct that stores the label names on the menu and * their corresponding actions when the user selects them @@ -291,6 +298,7 @@ gpointer watch_fifo(gpointer argv) #ifdef DEBUG printf("Removing onclick handler\n"); #endif + free(param); break; } @@ -320,71 +328,84 @@ gpointer watch_fifo(gpointer argv) #ifdef DEBUG printf("Removing onmenu handler\n"); #endif + free(param); break; } - // This block makes sure that the parameter after 'm' is well defined - int commas = 0; - int bars = 0; - if (len < 3){ - break; - } - if (param[0] == ',' || param[0] == '|'){ - printf("Action string cannot begin with ',' or '|'. Use '\\' to escape\n"); - break; - } - if (param[len-2] == '|' && param[len-3] != '\\'){ - printf("'|' can't be second of last. Use '\\' to escape\n"); - break; + // This block makes sure that the parameter after 'm' is ready to be processed + + char* initial = param; + //we save so that we can free later even if we increase the pointer + + while (param[0] == ',' || param[0] == '|'){ + // ignore delimiter characters in first spot + param++; + len--; } - if (param[len-1] == ',' && param[len-2] != '\\'){ - printf("',' can't be last. Use '\\' to escape\n"); - break; + while ((param[len-1] == ',' || param[len-1] == '|') && param[len-2] != '\\'){ + // ignore delimiters at the end too + // it also helps dividing the string in entries -> bars+1 + param[len-1] = '\0'; + len--; } - int i = 1; - while (i < len - 1){ - if (commas > bars + 1 || bars > commas) - break; - if (param[i] == ',' && param[i-1] != '\\') - commas++; - else if (param[i] == '|' && param[i-1] != '\\') + + // we can't accept 2 straight commas, as it becomes ambiguous + int straight = 0; + int bars = 0; + for (int i = 0; i < len; i++){ + if (param[i] == ',' && param[i-1] != '\\'){ + straight++; + if(straight == 2) + break; + } + else if (param[i] == '|' && param[i-1] != '\\' ){ + straight = 0; bars++; - i++; + } } - if (commas > bars + 1 || bars > commas){ - printf("Unbalanced ',' or '|'. Use '\\' to escape\n"); + if (straight == 2){ + printf("Two straight ',' found. Use '\\' to escape\n"); + free(initial); break; } // End of block that checks the parameter // Create the onmenu array which stores structs with name, action properties - menusize = commas; + menusize = bars + 1; onmenu = malloc(menusize * sizeof(struct item)); menu = gtk_menu_new(); int last = -1; int item = 0; - i = 1; - while(item < menusize && param[i] != '\0'){ + char lastFound = '|'; // what was the last delimiter processed + for(int i = 1; i < len; i++){ if (param[i] == ',' && param[i-1] != '\\'){ - onmenu[item].name = malloc((i-last) * sizeof(char)); - strncpy_esc(onmenu[item].name, param+last+1, i-last-1); - onmenu[item].name[i-last-1] = '\0'; + onmenu[item].name = save_word(param, i, last); last = i; + lastFound = ','; } else if (param[i] == '|' && param[i-1] != '\\'){ - onmenu[item].action = malloc((i-last) * sizeof(char)); - strncpy_esc(onmenu[item].action, param+last+1, i-last-1); - onmenu[item].action[i-last-1] = '\0'; + if (lastFound == ','){ // we have found a ',' so we read an action + onmenu[item].action = save_word(param, i, last); + } + else { //this is a label-only entry + onmenu[item].name = save_word(param, i, last); + onmenu[item].action = "\0"; + } last = i; + lastFound = '|'; item++; } - i++; } if(item < menusize){ //haven't read all actions because last one didn't end with a '|' - onmenu[item].action = malloc((len-last) * sizeof(char)); - strncpy_esc(onmenu[item].action, param+last+1, len-last-1); - onmenu[item].action[len-last-1] = '\0'; + if (lastFound == ','){ + onmenu[item].action = save_word(param, len, last); + } + else{ + onmenu[item].name = save_word(param, len, last); + onmenu[item].action = "\0"; + } } + // Now create the menu item widgets and attach them on the menu for (int i = 0; i < menusize; i++){ GtkWidget* w = gtk_menu_item_new_with_label(onmenu[i].name); @@ -392,7 +413,7 @@ gpointer watch_fifo(gpointer argv) g_signal_connect(G_OBJECT(w), "activate", G_CALLBACK(click_menu_item), NULL); } gtk_widget_show_all(menu); - + free(initial); break; default: fprintf(stderr, "Unknown command: '%c'\n", *buf); -- cgit From 853ce8c548904bf209dde6e53870fa771aa65879 Mon Sep 17 00:00:00 2001 From: Charalampos Kardaris Date: Wed, 4 Dec 2019 16:43:37 +0200 Subject: Fix on NULL param for m command and freeable pointers --- mktrayicon.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/mktrayicon.c b/mktrayicon.c index c1eb98d..caf3e85 100644 --- a/mktrayicon.c +++ b/mktrayicon.c @@ -323,8 +323,10 @@ gpointer watch_fifo(gpointer argv) } menusize = 0; - - if (param != NULL && *param == '\0') { + + if(!param) + break; + else if (*param == '\0') { #ifdef DEBUG printf("Removing onmenu handler\n"); #endif @@ -389,7 +391,8 @@ gpointer watch_fifo(gpointer argv) } else { //this is a label-only entry onmenu[item].name = save_word(param, i, last); - onmenu[item].action = "\0"; + onmenu[item].action = malloc(1); // pointer has to be freeable + onmenu[item].action = '\0'; } last = i; lastFound = '|'; @@ -402,7 +405,8 @@ gpointer watch_fifo(gpointer argv) } else{ onmenu[item].name = save_word(param, len, last); - onmenu[item].action = "\0"; + onmenu[item].action = malloc(1); + onmenu[item].action = '\0'; } } -- cgit From 0aa20c7e9d14fca6a25866eef29dcd37d02dabcd Mon Sep 17 00:00:00 2001 From: Charalampos Kardaris Date: Wed, 4 Dec 2019 16:53:52 +0200 Subject: Minor fix --- mktrayicon.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mktrayicon.c b/mktrayicon.c index caf3e85..78e6053 100644 --- a/mktrayicon.c +++ b/mktrayicon.c @@ -392,7 +392,7 @@ gpointer watch_fifo(gpointer argv) else { //this is a label-only entry onmenu[item].name = save_word(param, i, last); onmenu[item].action = malloc(1); // pointer has to be freeable - onmenu[item].action = '\0'; + *onmenu[item].action = '\0'; } last = i; lastFound = '|'; @@ -406,7 +406,7 @@ gpointer watch_fifo(gpointer argv) else{ onmenu[item].name = save_word(param, len, last); onmenu[item].action = malloc(1); - onmenu[item].action = '\0'; + *onmenu[item].action = '\0'; } } -- cgit From 35add2fc077dcc1436d36dc9f1e4df71651cd6a6 Mon Sep 17 00:00:00 2001 From: Charalampos Kardaris Date: Wed, 4 Dec 2019 17:42:23 +0200 Subject: Accept empty entries everywhere --- mktrayicon.c | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/mktrayicon.c b/mktrayicon.c index 78e6053..b414af3 100644 --- a/mktrayicon.c +++ b/mktrayicon.c @@ -335,23 +335,7 @@ gpointer watch_fifo(gpointer argv) } // This block makes sure that the parameter after 'm' is ready to be processed - - char* initial = param; - //we save so that we can free later even if we increase the pointer - - while (param[0] == ',' || param[0] == '|'){ - // ignore delimiter characters in first spot - param++; - len--; - } - while ((param[len-1] == ',' || param[len-1] == '|') && param[len-2] != '\\'){ - // ignore delimiters at the end too - // it also helps dividing the string in entries -> bars+1 - param[len-1] = '\0'; - len--; - } - - // we can't accept 2 straight commas, as it becomes ambiguous + // We can't accept 2 straight commas, as it becomes ambiguous int straight = 0; int bars = 0; for (int i = 0; i < len; i++){ @@ -367,7 +351,7 @@ gpointer watch_fifo(gpointer argv) } if (straight == 2){ printf("Two straight ',' found. Use '\\' to escape\n"); - free(initial); + free(param); break; } // End of block that checks the parameter @@ -379,7 +363,7 @@ gpointer watch_fifo(gpointer argv) int last = -1; int item = 0; char lastFound = '|'; // what was the last delimiter processed - for(int i = 1; i < len; i++){ + for(int i = 0; i < len; i++){ if (param[i] == ',' && param[i-1] != '\\'){ onmenu[item].name = save_word(param, i, last); last = i; @@ -417,7 +401,7 @@ gpointer watch_fifo(gpointer argv) g_signal_connect(G_OBJECT(w), "activate", G_CALLBACK(click_menu_item), NULL); } gtk_widget_show_all(menu); - free(initial); + free(param); break; default: fprintf(stderr, "Unknown command: '%c'\n", *buf); -- cgit From 14da73d07fa5ef1c602c4a83a00a181ca02bf004 Mon Sep 17 00:00:00 2001 From: Charalampos Kardaris Date: Thu, 12 Dec 2019 19:54:51 +0200 Subject: Fix in escape logic --- mktrayicon.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/mktrayicon.c b/mktrayicon.c index b414af3..634bc80 100644 --- a/mktrayicon.c +++ b/mktrayicon.c @@ -19,12 +19,18 @@ */ char* strncpy_esc(char *dest, const char *src, size_t n) { - size_t i; + size_t i = 0; size_t index = 0; - for (i = 0; i < n && src[i] != '\0'; i++){ - if (src[i] != '\\'){ + while (i < n && src[i] != '\0'){ + if (src[i] == '\\' && src[i+1] != '\0'){ + dest[index] = src[i+1]; + index++; + i+=2; + } + else{ dest[index] = src[i]; index++; + i++; } } for ( ; index < n; index++) -- cgit From e71588c05533d1ffc3fd3027965a1c1dc65ed05c Mon Sep 17 00:00:00 2001 From: Charalampos Kardaris Date: Thu, 12 Dec 2019 20:10:18 +0200 Subject: Add curly boys in single-statement if/for blocks --- mktrayicon.c | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/mktrayicon.c b/mktrayicon.c index 634bc80..3657297 100644 --- a/mktrayicon.c +++ b/mktrayicon.c @@ -33,8 +33,9 @@ char* strncpy_esc(char *dest, const char *src, size_t n) i++; } } - for ( ; index < n; index++) + for ( ; index < n; index++){ dest[index] = '\0'; + } return dest; } @@ -74,9 +75,11 @@ void tray_icon_on_click(GtkStatusIcon *status_icon, void click_menu_item(GtkMenuItem *menuitem, gpointer user_data) { const char* label = gtk_menu_item_get_label(menuitem); - for (int i = 0; i < menusize; i++) - if(strcmp(label,onmenu[i].name) == 0 && fork() == 0) + for (int i = 0; i < menusize; i++){ + if(strcmp(label,onmenu[i].name) == 0 && fork() == 0){ execl("/bin/sh", "sh", "-c", onmenu[i].action, (char *) NULL); + } + } } void tray_icon_on_menu(GtkStatusIcon *status_icon, guint button, @@ -85,8 +88,9 @@ void tray_icon_on_menu(GtkStatusIcon *status_icon, guint button, #ifdef DEBUG printf("Popup menu\n"); #endif - if (menusize) + if (menusize){ gtk_menu_popup_at_pointer((GtkMenu *)menu, NULL); + } } gboolean set_tooltip(gpointer data) @@ -330,8 +334,9 @@ gpointer watch_fifo(gpointer argv) menusize = 0; - if(!param) + if(!param){ break; + } else if (*param == '\0') { #ifdef DEBUG printf("Removing onmenu handler\n"); @@ -347,8 +352,9 @@ gpointer watch_fifo(gpointer argv) for (int i = 0; i < len; i++){ if (param[i] == ',' && param[i-1] != '\\'){ straight++; - if(straight == 2) + if(straight == 2){ break; + } } else if (param[i] == '|' && param[i-1] != '\\' ){ straight = 0; @@ -411,7 +417,7 @@ gpointer watch_fifo(gpointer argv) break; default: fprintf(stderr, "Unknown command: '%c'\n", *buf); - if (param != NULL) { + if (param != NULL){ free(param); } } -- cgit From 1130daed1ef09f1926f954cb2ecea00afa309957 Mon Sep 17 00:00:00 2001 From: Charalampos Kardaris Date: Thu, 12 Dec 2019 20:25:02 +0200 Subject: Updated README --- README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d4fe5bb..089ac5b 100644 --- a/README.md +++ b/README.md @@ -43,10 +43,15 @@ Quoted strings are terminated by a matching quote at the end of a line to continue a quoted string, prefix it with a `\`. The m(enu) command uses `,` as a delimiter between label and command and `|` as a delimiter -between entries (label+command). If you want to use these 2 characters in a label or command, you have to escape -them with `\`. - -Example command: `echo "m Browser,firefox|Terminal,xterm" > /tmp/test` (where `mkfifo /tmp/test` has been executed before) +between entries (label+command). +If you want to use these 2 characters in a label or command, you have to escape +them with `\`. +If you want to have an entry with just a label and no command to be executed, you can ommit the +`,` part. +If you want an empty label, you can just add a second `|` delimiter after the previous one. +If you want a command to be executed upon selection of an empty label, you can add `,` after the previous `|` + +Example command: `echo "m Browser,firefox|Terminal,xterm|Label-only||,chromium" > /tmp/test` (where `mkfifo /tmp/test` has been executed before) ## Why? -- cgit From 6454a4451ffb6aeef92efda4c3fd93e52d30ffa7 Mon Sep 17 00:00:00 2001 From: Charalampos Kardaris Date: Thu, 12 Dec 2019 20:31:47 +0200 Subject: Fix README --- README.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 089ac5b..e05e9f0 100644 --- a/README.md +++ b/README.md @@ -43,13 +43,10 @@ Quoted strings are terminated by a matching quote at the end of a line to continue a quoted string, prefix it with a `\`. The m(enu) command uses `,` as a delimiter between label and command and `|` as a delimiter -between entries (label+command). -If you want to use these 2 characters in a label or command, you have to escape -them with `\`. -If you want to have an entry with just a label and no command to be executed, you can ommit the -`,` part. -If you want an empty label, you can just add a second `|` delimiter after the previous one. -If you want a command to be executed upon selection of an empty label, you can add `,` after the previous `|` +between entries (label+command).If you want to use these 2 characters in a label or command, you have to escape +them with `\`. If you want to have an entry with just a label and no command to be executed, you can omit the +`,` part. If you want an empty label (e.g. as a separator), you can just add a second `|` delimiter after the previous one. +If you want a command to be executed upon selection of an empty label, you can add `,` after the previous `|`. Example command: `echo "m Browser,firefox|Terminal,xterm|Label-only||,chromium" > /tmp/test` (where `mkfifo /tmp/test` has been executed before) -- cgit