From 42b63d91290ba85bb9a6a95f33ad0de2f9d64e7a Mon Sep 17 00:00:00 2001 From: "B. Stack" Date: Thu, 29 Sep 2022 15:16:37 -0400 Subject: WIP: minimum viable product for status.c The absolute bare minimum, working C program that displays the capslock and numlock indicators in system tray. It has hardcoded values for icons and named pipes, and no config parsing. Possible config libraries include: inih, libini-config5, libminini, libiniparser1 --- .gitignore | 3 + experimental/Makefile | 8 +- experimental/status.c | 713 +++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 721 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 4e1c82e..1c3ea63 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ debian/files *.substvars gitmessage debian +status +*.o +old/ diff --git a/experimental/Makefile b/experimental/Makefile index 27d84c4..2bb7fca 100644 --- a/experimental/Makefile +++ b/experimental/Makefile @@ -18,11 +18,15 @@ CC = gcc # to get full debug symbols, add to both FLAGS: -g # to make all warnings act as errors: -Wall -Weffc++ -Wextra -Wsign-conversion -Werror CXXFLAGS = -g -std=c++17 -Wall -Weffc++ -Wextra -Wsign-conversion -Werror -CCFLAGS = -g -Wall -Wextra -Wsign-conversion -Werror `pkg-config x11 --cflags` +CCFLAGS = -g -Wno-deprecated-declarations \ + `pkg-config --cflags x11` \ + `pkg-config --cflags gtk+-3.0` # to remove all debug symbols: -s # to add full debug symbols: -g -LDFLAGS = -g `pkg-config x11 --libs` +LDFLAGS = -g \ + `pkg-config --libs x11` \ + `pkg-config --libs gtk+-3.0` src = $(wildcard *.c) obj = $(src:.c=.o) diff --git a/experimental/status.c b/experimental/status.c index 6357b62..f968af1 100644 --- a/experimental/status.c +++ b/experimental/status.c @@ -7,18 +7,619 @@ * Title: Proof of Concept C utility for polling capslock and numlock * Purpose: Demonstrate these can be done in C, with the eventual goal of rewriting keyboard-leds-trayicons entirely in C to avoid the "sleep 0.75" in ps output * History: + * minimum viable product * Usage: * References: * https://github.com/Cairo-Dock/cairo-dock-plug-ins/blob/master/keyboard-indicator/src/applet-xklavier.c#L124 * https://github.com/oco2000/xfce4-kbdleds-plugin/blob/fe753d9d0f8a720a35a32f5f556b8fbead798d20/panel-plugin/xkbleds.c + * https://stackoverflow.com/questions/60897833/programmatically-calling-mainint-argc-char-argv-in-c + * https://stackoverflow.com/questions/18649547/how-to-find-the-length-of-argv-in-c + * https://www.geeksforgeeks.org/fork-system-call/ + * https://stackoverflow.com/questions/43326857/multiple-fork-in-c-program + * https://stackoverflow.com/questions/14173268/fifo-files-and-read-write + * https://ftp.gnu.org/old-gnu/Manuals/glibc-2.2.3/html_chapter/libc_15.html * Improvements: * Write main keyboard-led-trayicons logic, which includes parsing config file (libconfig?) * Write pipe connectivity to mktrayicon or integrate it entirely + * https://qnaplus.com/c-program-to-sleep-in-milliseconds/ * Dependencies: libx11-dev */ +#include +#include +#include #include +#include #include +#include +#include #include +#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 = 0; + size_t index = 0; + 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++) { + dest[index] = '\0'; + } + 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 + */ +struct item { + char *name; + char *action; +} * onmenu; +char *onscrollup = NULL; +char *onscrolldown = NULL; +char *onmiddleclick = NULL; +int menusize = 0; // number of menu entries +GtkWidget *menu = NULL; + +GtkStatusIcon *icon; +char *onclick = NULL; + +void tray_icon_on_click(GtkStatusIcon *status_icon, gpointer user_data) { + if (onclick != NULL && fork() == 0) { + execl("/bin/sh", "sh", "-c", onclick, (char *)NULL); + } +} + +void tray_icon_on_middleclick(GtkStatusIcon *status_icon, GdkEventButton *event, + gpointer user_data) { + if (2 == event->button) { + if (onmiddleclick == NULL) { +#ifdef DEBUG + printf("middleclick, but no command specified\n"); +#endif + } else { +#ifdef DEBUG + printf("middleclick\n"); +#endif + if (onmiddleclick != NULL && fork() == 0) { + execl("/bin/sh", "sh", "-c", onmiddleclick, (char *)NULL); + } + } + } +} + +/* + * 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); + } +} + +void tray_icon_on_scroll(GtkStatusIcon *status_icon, GdkEventScroll *event, + gpointer user_data) { + char *i = NULL; + switch (event->direction) { + case GDK_SCROLL_UP: + i = "up"; + if (onscrollup != NULL && fork() == 0) + execl("/bin/sh", "sh", "-c", onscrollup, (char *)NULL); + break; + case GDK_SCROLL_DOWN: + i = "down"; + if (onscrolldown != NULL && fork() == 0) + execl("/bin/sh", "sh", "-c", onscrolldown, (char *)NULL); + break; + } + if (i != NULL) { +#ifdef DEBUG + printf("scroll %s\n",i); +#endif + } +} + +gboolean set_tooltip(gpointer data) { + char *p = (char *)data; + if (*p == '\0') { +#ifdef DEBUG + printf("Removing tooltip\n"); +#endif + gtk_status_icon_set_has_tooltip(icon, FALSE); + return FALSE; + } + +#ifdef DEBUG + printf("Setting tooltip to '%s'\n", p); +#endif + gtk_status_icon_set_tooltip_text(icon, p); + free(data); + return FALSE; +} + +gboolean set_icon(gpointer data) { + char *p = (char *)data; +#ifdef DEBUG + printf("Setting icon to '%s'\n", p); +#endif + if (strchr(p, '/')) { + gtk_status_icon_set_from_file(icon, p); + } else { + gtk_status_icon_set_from_icon_name(icon, p); + } + free(data); + return FALSE; +} + +gboolean set_visible(gpointer data) { + gtk_status_icon_set_visible(icon, data == 0 ? FALSE : TRUE); + return FALSE; +} + +gboolean do_quit(gpointer data) { + gtk_main_quit(); + return FALSE; +} + +gpointer watch_fifo(gpointer argv) { + char *buf = malloc(1024 * sizeof(char)); + char cmd; + char quote; + char *param; + char *tmp = malloc(1024 * sizeof(char)); + char *read; + size_t len, i; + char *fifo_path = (char *)argv; + FILE *fifo; + struct stat fifo_st; + +/* outer is for open */ +outer: + while (1) { + if (stat(fifo_path, &fifo_st) != 0) { + perror("FIFO does not exist, exiting\n"); + gdk_threads_add_idle(do_quit, fifo); + return NULL; + } + + fifo = fopen(fifo_path, "r"); + + if (fifo == NULL) { + perror("FIFO went away, exiting\n"); + gdk_threads_add_idle(do_quit, fifo); + return NULL; + } + + /* inner is for read */ + while (1) { + read = fgets(buf, 1024 * sizeof(char), fifo); + + if (read == NULL) { + /* no more data in pipe, reopen and block */ + fclose(fifo); + goto outer; + } + + /* trim string */ + while ((*read == '\n' || *read == ' ' || *read == '\t') && + *read != '\0') { + read++; + } + + if (*read == '\0') { + /* empty command */ + continue; + } + + cmd = *read; + len = strlen(read); + if (len < 3) { + param = NULL; + } else if (*(read + 2) != '\'' && *(read + 2) != '"') { + // unquoted string + read += 2; + len -= 2; + // trim trailing whitespace + i = len - 1; + while (i > 0) { + if (!isspace(read[i])) { + len = i + 1; + read[len] = '\0'; + break; + } + i -= 1; + } + param = malloc((len + 1) * sizeof(char)); + strncpy(param, read, len + 1); + } else { + // quoted string + quote = *(read + 2); + read += 3; + len -= 3; + *tmp = '\0'; + *(tmp + 1024 - 1) = '\0'; + // keep track of what we have so far + strncpy(tmp, read, 1023); + + // now keep reading until we have the end quote + while (1) { + // check for terminating ' + if (len != 0) { + // search backwards past whitespace + i = len - 1; + while (i > 0) { + if (!isspace(tmp[i])) { + break; + } + i -= 1; + } + if (tmp[i] == quote) { + // maybe the end! + // let's make sure it isn't escaped + if (i >= 2 && tmp[i - 2] == '\\') { + } else { + // it's not! + // we're done. + // trim off the ' and + // any whitespace we walked past + len = i; + tmp[len] = '\0'; + break; + } + } + } + + if (len == 1023) { + // we can't read any more + // but also haven't found the end + // forcibly terminate the string + fprintf(stderr, "Quoted string too long (max 1023 chars)\n"); + break; + } + + // we don't have the end of the string yet + read = fgets(buf, 1024 * sizeof(char), fifo); + if (read == NULL) { + /* no more data in pipe, reopen and block */ + fclose(fifo); + goto outer; + } + // note that we don't trim here, because we're + // in a quoted string. + strncpy(tmp + len, read, 1023 - len); + len += strlen(tmp + len); + } + + // quoted string is now in param[0:len] + param = malloc((len + 1) * sizeof(char)); + strncpy(param, tmp, len + 1); + } + + switch (cmd) { + case 'q': + gdk_threads_add_idle(do_quit, param); + if (param != NULL) { + free(param); + } + break; + case 't': /* tooltip */ + gdk_threads_add_idle(set_tooltip, param); + break; + case 'i': /* icon */ + gdk_threads_add_idle(set_icon, param); + break; + case 'h': /* hide */ + gdk_threads_add_idle(set_visible, (void *)0); + if (param != NULL) { + free(param); + } + break; + case 's': /* show */ + gdk_threads_add_idle(set_visible, (void *)1); + if (param != NULL) { + free(param); + } + break; + case 'c': /* click */ + if (onclick != NULL) { + free(onclick); + onclick = NULL; + } + + if (param != NULL && *param == '\0') { +#ifdef DEBUG + printf("Removing onclick handler\n"); +#endif + free(param); + break; + } + + onclick = param; +#ifdef DEBUG + printf("Setting onclick handler to '%s'\n", onclick); +#endif + break; + case 'm': /* menu */ + 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) { + break; + } else if (*param == '\0') { +#ifdef DEBUG + printf("Removing onmenu handler\n"); +#endif + free(param); + break; + } + + // This block makes sure that the parameter after 'm' is ready to be + // processed 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++; + } + } + if (straight == 2) { + printf("Two straight ',' found. Use '\\' to escape\n"); + free(param); + break; + } + // End of block that checks the parameter + + // Create the onmenu array which stores structs with name, action + // properties + menusize = bars + 1; + onmenu = malloc(menusize * sizeof(struct item)); + menu = gtk_menu_new(); + int last = -1; + int item = 0; + char lastFound = '|'; // what was the last delimiter processed + for (int i = 0; i < len; i++) { + if (param[i] == ',' && param[i - 1] != '\\') { + onmenu[item].name = save_word(param, i, last); + last = i; + lastFound = ','; + } else if (param[i] == '|' && param[i - 1] != '\\') { + 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 = malloc(1); // pointer has to be freeable + *onmenu[item].action = '\0'; + } + last = i; + lastFound = '|'; + item++; + } + } + if (item < menusize) { // haven't read all actions because last one + // didn't end with a '|' + if (lastFound == ',') { + onmenu[item].action = save_word(param, len, last); + } else { + onmenu[item].name = save_word(param, len, last); + onmenu[item].action = malloc(1); + *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; + if (0 == strlen(onmenu[i].name) || (!strncmp(onmenu[i].name, "-----", 5))) { + w = gtk_separator_menu_item_new() ; + } else { + w = gtk_menu_item_new_with_label(onmenu[i].name); + g_signal_connect(G_OBJECT(w), "activate", G_CALLBACK(click_menu_item), + NULL); + } + gtk_menu_shell_append(GTK_MENU_SHELL(menu), w); + } + gtk_widget_show_all(menu); + free(param); + break; + case 'R': /* mouse scroll up */ + if (onscrollup != NULL) { + free(onscrollup); + } + if (!param || (*param == '\0')) { +#ifdef DEBUG + printf("Removing scrollup command\n"); + onscrollup = NULL; +#endif + } else { +#ifdef DEBUG + printf("Setting scrollup command\n"); +#endif + onscrollup = malloc(strlen(param)); + strncpy(onscrollup, param, len + 1); + } + break; + case 'r': /* mouse scroll down */ + if (onscrolldown != NULL) { + free(onscrolldown); + } + if (!param || (*param == '\0')) { +#ifdef DEBUG + printf("Removing scrolldown command\n"); + onscrolldown = NULL; +#endif + } else { +#ifdef DEBUG + printf("Setting scrolldown command\n"); +#endif + onscrolldown = malloc(strlen(param)); + strncpy(onscrolldown, param, len + 1); + } + break; + case 'S': /* mouse middle click */ + if (onmiddleclick != NULL) { + free(onmiddleclick); + } + if (!param || (*param == '\0')) { +#ifdef DEBUG + printf("Removing middle click command\n"); + onmiddleclick = NULL; +#endif + } else { +#ifdef DEBUG + printf("Setting middleclick command\n"); +#endif + onmiddleclick = malloc(strlen(param)); + strncpy(onmiddleclick, param, len + 1); + } + break; + default: + fprintf(stderr, "Unknown command: '%c'\n", *buf); + if (param != NULL) { + free(param); + } + } + + gdk_flush(); + } + } + return NULL; +} + +static GtkStatusIcon *create_tray_icon(char *start_icon) { + GtkStatusIcon *tray_icon; + + if (strchr(start_icon, '/')) { + tray_icon = gtk_status_icon_new_from_file(start_icon); + } else { + tray_icon = gtk_status_icon_new_from_icon_name(start_icon); + } + g_signal_connect(G_OBJECT(tray_icon), "activate", + G_CALLBACK(tray_icon_on_click), NULL); + g_signal_connect(G_OBJECT(tray_icon), "popup-menu", + G_CALLBACK(tray_icon_on_menu), NULL); + g_signal_connect(G_OBJECT(tray_icon), "scroll-event", + G_CALLBACK(tray_icon_on_scroll), NULL); + g_signal_connect(G_OBJECT(tray_icon), "button-release-event", + G_CALLBACK(tray_icon_on_middleclick), NULL); + gtk_status_icon_set_visible(tray_icon, TRUE); + + return tray_icon; +} + +int print_usage(char **argv) { + printf("Usage: %s [-i ICON] [-t TOOLTIP] [-h] [FIFO]\n", *argv); + printf("Create a system tray icon as specified\n"); + printf("\n"); + printf(" -i ICON\tUse the specified ICON when initializing\n"); + printf(" -t TOOLTIP\tUse the specified TOOLTIP when initializing\n"); + printf(" -h \t\tDisplay this help message\n"); + printf("\n"); + printf("If a FIFO is not provided, mktrayicon will run until killed\n"); + printf("Report bugs at https://github.com/jonhoo/mktrayicon\n"); + return 0; +} + +int fmain(int argc, char **argv) { + char *start_icon = "none"; + char *tooltip = NULL; + char *pipe = NULL; + GThread *reader; + + XInitThreads(); /* see http://stackoverflow.com/a/18690540/472927 */ + gtk_init(&argc, &argv); + + if (argc == 1) { + return print_usage(argv); + } + + int c; + while ((c = getopt(argc, argv, "i:t:h")) != -1) + switch (c) { + case 'i': + start_icon = optarg; + break; + case 't': + tooltip = optarg; + break; + case 'h': + return print_usage(argv); + case '?': + fprintf(stderr, "Unknown option: %c\n", optopt); + return 1; + } + + icon = create_tray_icon(start_icon); + + if (tooltip) { + gtk_status_icon_set_tooltip_text(icon, tooltip); + } + + /* optind holds the index of the next argument to be parsed */ + /* getopt moved positional arguments (if there were any) to the end of the + * argv array, without parsing them */ + /* so if there were only non-positional arguments, all arguments have been + * parsed and optind will be equal to argc */ + if (optind < argc) { + pipe = argv[optind]; + reader = g_thread_new("watch_fifo", watch_fifo, pipe); + } + + gtk_main(); + return 0; +} Display* dpy; @@ -30,11 +631,121 @@ int get_indicator(Display* dpy, char* indicator) { return st; } -int main() { +int main(int argc, char **argv) { + FILE *stream; dpy = XOpenDisplay( NULL ); int status_capslock = get_indicator(dpy, "Caps Lock"); int status_numlock = get_indicator(dpy, "Num Lock"); printf("Capslock: %d\tNumlock: %d\n",status_capslock,status_numlock); + char *fifo_name_C = "/tmp/fifo1"; + char *fifo_name_N = "/tmp/fifo2"; printf("done\n"); + char *_argv_C[] = { "fmain", fifo_name_C, NULL }; + char *_argv_N[] = { "fmain", fifo_name_N, NULL }; + int count_C = 0; + int count_N = 0; + int fd_C, fd_N; + struct stat st_C, st_N; + char *msg; + if (stat(fifo_name_C, &st_C) != 0) + mkfifo(fifo_name_C, 0666); + if (stat(fifo_name_N, &st_N) != 0) + mkfifo(fifo_name_N, 0666); + while (_argv_C[++count_C]); + while (_argv_N[++count_N]); + //fd_C = open(fifo_name_C, O_WRONLY); + if (fork() == 0) { + // parent1->child 1 + if (fork() == 0) { + // parent1->child1->child1 + fmain(count_C,_argv_C); + } + else { + // parent1->child1 + fd_C = open(fifo_name_C, O_WRONLY); + stream = fdopen(fd_C,"w"); + msg = "m quit,echo 'q' > /tmp/fifo1\n"; + printf("sending to fd_C %d message %s",fd_C,msg); + fprintf(stream,msg); + if (status_capslock) + msg = "i /home/bgstack15/dev/keyboard-leds-trayicons/src/usr/share/icons/hicolor/scalable/status/capslock-on.svg\n"; + else + msg = "i /home/bgstack15/dev/keyboard-leds-trayicons/src/usr/share/icons/hicolor/scalable/status/capslock-off.svg\n"; + printf("sending to fd_C %d message %s",fd_C,msg); + fprintf(stream,msg); + fclose(stream); + }; + } + else { + // parent 1 + if (fork() == 0) { + // parent1->child2 + fmain(count_N,_argv_N); + } + else { + // parent 1 + fd_N = open(fifo_name_N, O_WRONLY); + stream = fdopen(fd_N,"w"); + msg = "i battery\n"; + printf("sending to fd_N %d message %s",fd_N,msg); + fprintf(stream,msg); + msg = "m say hello,yad 'hello'|quit,echo 'q' > /tmp/fifo2\n"; + printf("sending to fd_N %d message %s",fd_N,msg); + fprintf(stream,msg); + fclose(stream); + }; + // still parent 1 + // main logic loop happens now + //while (access("/tmp/stop-keyboard-leds",F_OK) != 0) { + fd_C = open(fifo_name_C, O_WRONLY); + fd_N = open(fifo_name_N, O_WRONLY); + int status_caps_old = status_capslock; + int status_num_old = status_numlock; + while (1) { + //ueep(1000000); 1 second + //ueep( 1000); 1 millisecond + usleep( 100000); // WORKHERE: should be configurable + status_capslock = get_indicator(dpy, "Caps Lock"); + status_numlock = get_indicator(dpy, "Num Lock"); + //printf("Capslock: %d\tNumlock: %d\n",status_capslock,status_numlock); + if(status_capslock != status_caps_old) { + stream = fdopen(fd_C,"w"); + if (stream > 0) { + if(0 == status_capslock) { + msg = "i /home/bgstack15/dev/keyboard-leds-trayicons/src/usr/share/icons/hicolor/scalable/status/capslock-off.svg\n"; + //printf("sending to fd_C %d message %s",fd_C,msg); + printf("capslock off\n"); + fprintf(stream,msg); + } + else { + msg = "i /home/bgstack15/dev/keyboard-leds-trayicons/src/usr/share/icons/hicolor/scalable/status/capslock-on.svg\n"; + printf("capslock on\n"); + fprintf(stream,msg); + }; + status_caps_old = status_capslock; + fflush(stream); + //fclose(stream); + }; + }; + if(status_numlock != status_num_old) { + stream = fdopen(fd_N,"w"); + if (stream > 0) { + if(0 == status_numlock) { + msg = "i /home/bgstack15/dev/keyboard-leds-trayicons/src/usr/share/icons/hicolor/scalable/status/numlock-off.svg\n"; + //printf("sending to fd_N %d message %s",fd_N,msg); + printf("numlock off\n"); + fprintf(stream,msg); + } + else { + msg = "i /home/bgstack15/dev/keyboard-leds-trayicons/src/usr/share/icons/hicolor/scalable/status/numlock-on.svg\n"; + printf("numlock on\n"); + fprintf(stream,msg); + }; + status_num_old = status_numlock; + fflush(stream); + }; + }; + }; + }; return 0; } -- cgit