/* * File: keyboard-leds-trayicons.c * Location: https://bgstack15.ddns.net/cgit/keyboard-leds-trayicons/ * Authors: bgstack15, jonhoo * Startdate: 2022-09-28-4 13:30 * SPDX-License-Identifier: GPL-3.0 * Title: Proof of Concept C utility for polling capslock and numlock * Purpose: Capslock and numlock indicators for system tray, in C * History: * 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 * https://qnaplus.com/c-program-to-sleep-in-milliseconds/ * https://stackoverflow.com/questions/47107311/sending-signal-from-child-to-parent * https://stackoverflow.com/questions/108183/how-to-prevent-sigpipes-or-handle-them-properly * https://cboard.cprogramming.com/c-programming/150795-signal-sigaction-conversion-printable-thread.html * http://maemo.cloud-7.de/irclogs/freenode/_devuan-dev/_devuan-dev.2022-10-01.log.html * https://stackoverflow.com/questions/190229/where-is-the-itoa-function-in-linux/29544416#29544416 * https://www.geeksforgeeks.org/socket-programming-cc/ * Improvements: * Maybe someday use connect_wait from https://monoxid.net/c/socket-connection-timeout/ when it works * Fix the occasional "malloc(): invalid next size (unsorted)" when pressing capslock. * Dependencies: * build: libx11-dev, libinih-dev, libgtk-3-dev, libxdo-dev * Documentation: * features include: * still operate when only one trayicon has exited * exit when both trayicons have exited * use sockets to communicate to trayicons * use signals to clean up when trayicons exit * combined sockets and fifos: run with '-s' to use Unix socket, if compiled with ENABLE_SOCKETS=1 * left-click the icon to toggle state using xdotool (or libxdo if ENABLE_LIBXDO), disable with -n * show tooltips */ #include "keyboard-leds-trayicons.h" #ifdef ENABLE_SOCKETS int use_sockets = 0; #endif int debug = 0; Display* dpy; typedef struct { const char* caps_on; const char* caps_off; const char* num_on; const char* num_off; int sleep_microseconds; const char* caps_fifo; const char* num_fifo; } configuration; char* conffile = "none"; const int BUFLEN = 1000; char msg[1000]; #ifdef ENABLE_SOCKETS char child_pid_str[1000]; #endif int closed_icons = 0; configuration config; struct stat st_C, st_N; pid_t pid, pid_C, pid_N; int fd_C, fd_N; int click_to_switch = 1; // invoke with -n #ifdef ENABLE_SOCKETS void remove_first_newline(char *string) { // ref: https://stackoverflow.com/a/9628684 int ii; for(ii = 0;;ii++) { if (string[ii] == '\n') { string[ii] = '\0'; break; } } } #endif // useful only when using fifo void sigfun_main(int sig) { // pressed CTRL+C, which was set with sigaction() in Step 7 printf("Pressed CTRL+C\n"); #ifdef ENABLE_SOCKETS if (use_sockets) { } else { #endif unlink(config.caps_fifo); unlink(config.num_fifo); #ifdef ENABLE_SOCKETS } #endif (void) signal(SIGINT, SIG_DFL); exit(0); } void sigfun_child(int sig) { // killed kill(pid,SIGUSR1); (void) signal(SIGTERM, SIG_DFL); exit(0); } /* reverse: reverse string s in place */ void klt_reverse(char s[]) { int i, j; char c; for (i = 0, j = strlen(s)-1; i 0); /* delete it */ if (sign < 0) s[i++] = '-'; s[i] = '\0'; klt_reverse(s); } /* START inclusion of mktrayicon.c, 536 lines */ /* * 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) { #ifdef ENABLE_LIBXDO if (strcmp(onclick,"CAPS") == 0) { // xdo_new() does not like being initialized into a global variable? xdo_t *xdo_context = xdo_new(NULL); xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Caps_Lock", 0); xdo_free(xdo_context); } else if (strcmp(onclick,"NUM") == 0) { xdo_t *xdo_context = xdo_new(NULL); xdo_send_keysequence_window(xdo_context, CURRENTWINDOW, "Num_Lock", 0); xdo_free(xdo_context); } else #endif if (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) { if (debug) printf("middleclick, but no command specified\n"); } else { if (debug) printf("middleclick\n"); 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) { if (debug) printf("Popup menu\n"); 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) { if (debug) printf("scroll %s\n",i); } } gboolean set_tooltip(gpointer data) { char *p = (char *)data; if (*p == '\0') { if (debug) printf("Removing tooltip\n"); gtk_status_icon_set_has_tooltip(icon, FALSE); return FALSE; } if (debug) printf("Setting tooltip to '%s'\n", p); gtk_status_icon_set_tooltip_text(icon, p); free(data); return FALSE; } gboolean set_icon(gpointer data) { char *p = (char *)data; if (debug) printf("Setting icon to '%s'\n", p); 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; } #ifdef ENABLE_SOCKETS gpointer watch_socket(gpointer data) { char buffer[BUFLEN]; int enable = 1; int thispid = getpid(); char *sockfile_local = (char *)data; int server_sockfd, client_sockfd; socklen_t client_len; ssize_t nbytes_read; struct sockaddr_un client_address, server_address; char *rread, *param; char cmd, quote; size_t len, i; char *tmp = malloc(1024 * sizeof(char)); char *buf = malloc(1024 * sizeof(char)); server_sockfd = socket(AF_LOCAL, SOCK_STREAM, 0); if (server_sockfd == -1) { perror("server socket"); exit(EXIT_FAILURE); } if (setsockopt(server_sockfd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)) < 0) { perror("server setsockopt(SO_REUSEADDR) failed"); exit(EXIT_FAILURE); } server_address.sun_family = AF_LOCAL; strncpy(server_address.sun_path,sockfile_local,strlen(sockfile_local)); if (bind(server_sockfd,(struct sockaddr*)&server_address, sizeof(server_address) ) == -1 ) { perror("server bind"); exit(EXIT_FAILURE); } if (listen(server_sockfd, 5) == -1) { perror("server listen"); exit(EXIT_FAILURE); } fprintf(stderr, "trayicon %d listening on socket %s\n", thispid, sockfile_local); signal(SIGPIPE, SIG_IGN); while (1) { client_len = sizeof(client_address); client_sockfd = accept( server_sockfd, (struct sockaddr*)&client_address, &client_len ); while ((nbytes_read = read(client_sockfd, buffer, BUFLEN)) > 0) { rread = buffer; //printf("received:\n"); //write(STDOUT_FILENO, buffer, nbytes_read); // START copy-pasted in from watch_fifo /* trim string */ while ((*rread == '\n' || *rread == ' ' || *rread == '\t') && *rread != '\0') { rread++; } if (*rread == '\0') { /* empty command */ continue; } cmd = *rread; len = strlen(rread); if (len < 3) { param = NULL; } else if (*(rread + 2) != '\'' && *(rread + 2) != '"') { // unquoted string rread += 2; len -= 2; // trim trailing whitespace i = len - 1; while (i > 0) { if (!isspace(rread[i])) { len = i + 1; rread[len] = '\0'; break; } i -= 1; } if (debug >= 2){ fprintf(stderr,"malloc1: %i: ",(len + 1)); }; param = malloc((len + 1) * sizeof(char)); if (debug >= 2){fprintf(stderr,"DONE-malloc1\n");}; strncpy(param, rread, len + 1); } else { // quoted string quote = *(rread + 2); rread += 3; len -= 3; *tmp = '\0'; *(tmp + 1024 - 1) = '\0'; // keep track of what we have so far strncpy(tmp, rread, 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; } // FIXME unfinished imported logic // we don't have the end of the string yet //read = fgets(buf, 1024 * sizeof(char), fifo); // nbytes_read = read(client_sockfd, buffer, BUFLEN) //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, rread, 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); } remove_first_newline(param); 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') { if (debug) printf("Removing onclick handler\n"); free(param); break; } onclick = param; if (debug) printf("Setting onclick handler to '%s'\n", onclick); break; case 'm': /* menu */ if (debug >= 2) printf("Building menu from string: \"%s\"\n", 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) { break; } else if (*param == '\0') { if (debug) printf("Removing onmenu handler\n"); 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')) { if (debug) printf("Removing scrollup command\n"); onscrollup = NULL; } else { if (debug) printf("Setting scrollup command\n"); onscrollup = malloc(strlen(param)); strncpy(onscrollup, param, len + 1); } break; case 'r': /* mouse scroll down */ if (onscrolldown != NULL) { free(onscrolldown); } if (!param || (*param == '\0')) { if (debug) printf("Removing scrolldown command\n"); onscrolldown = NULL; } else { if (debug) printf("Setting scrolldown command\n"); onscrolldown = malloc(strlen(param)); strncpy(onscrolldown, param, len + 1); } break; case 'S': /* mouse middle click */ if (onmiddleclick != NULL) { free(onmiddleclick); } if (!param || (*param == '\0')) { if (debug) printf("Removing middle click command\n"); onmiddleclick = NULL; } else { if (debug) printf("Setting middleclick command\n"); 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(); // STOP copy-pasted in from watch_fifo } close(client_sockfd); } return EXIT_SUCCESS; } #endif 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') { if (debug) printf("Removing onclick handler\n"); free(param); break; } onclick = param; if (debug) printf("Setting onclick handler to '%s'\n", onclick); 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') { if (debug) printf("Removing onmenu handler\n"); 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')) { if (debug) printf("Removing scrollup command\n"); onscrollup = NULL; } else { if (debug) printf("Setting scrollup command\n"); onscrollup = malloc(strlen(param)); strncpy(onscrollup, param, len + 1); } break; case 'r': /* mouse scroll down */ if (onscrolldown != NULL) { free(onscrolldown); } if (!param || (*param == '\0')) { if (debug) printf("Removing scrolldown command\n"); onscrolldown = NULL; } else { if (debug) printf("Setting scrolldown command\n"); onscrolldown = malloc(strlen(param)); strncpy(onscrolldown, param, len + 1); } break; case 'S': /* mouse middle click */ if (onmiddleclick != NULL) { free(onmiddleclick); } if (!param || (*param == '\0')) { if (debug) printf("Removing middle click command\n"); onmiddleclick = NULL; } else { if (debug) printf("Setting middleclick command\n"); 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(void* item) { char *start_icon = "none"; char *tooltip = NULL; void *pipe = NULL; GThread *reader; XInitThreads(); /* see http://stackoverflow.com/a/18690540/472927 */ //char *argv[] = {}; char **_argv[] = { NULL }; gtk_init(0, _argv); 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 */ //pipe = argv[optind]; #ifdef ENABLE_SOCKETS if (use_sockets) { pipe = (int *) item; reader = g_thread_new("watch_socket", watch_socket, pipe); } else { #endif pipe = (FILE *) item; reader = g_thread_new("watch_fifo", watch_fifo, pipe); #ifdef ENABLE_SOCKETS } #endif gtk_main(); return 0; } /* STOP inclusion of mktrayicon.c */ int get_indicator(Display* dpy, char* indicator) { // where indicator is one of ["Num Lock", "Caps Lock"] Atom lockIndicator = XInternAtom(dpy, indicator, False); int st; XkbGetNamedIndicator(dpy, lockIndicator, NULL, &st, NULL, NULL); return st; } // slightly modified from example https://github.com/benhoyt/inih static int handler(void* user, const char* section, const char* name, const char* value) { // KLT does not need section information at all. configuration *pconfig = (configuration*)user; #define MATCH(n, m, func) if (strcmp(name, n) == 0) { pconfig->m = func(value); } MATCH("KLT_CAPS_ON_ICON", caps_on, strdup) else MATCH("KLT_CAPS_OFF_ICON", caps_off, strdup) else MATCH("KLT_NUM_ON_ICON", num_on, strdup) else MATCH("KLT_NUM_OFF_ICON", num_off, strdup) else MATCH("KLT_SLEEP_MICROSECONDS", sleep_microseconds, atoi) else MATCH("KLT_CAPS_FIFO", caps_fifo, strdup) else MATCH("KLT_NUM_FIFO", num_fifo, strdup) else { return 0; /* unknown section/name, error */ } return 1; } int send_message(char *message, const int sock, FILE *stream, const pid_t child_pid, const char *fifo, const int quit) { /* smart handler to simplify the Step 6 logic that mostly repeats a bunch of ifdefs */ // using a local variable makes the rest of this function operate way better! char sm_message[1000]; strcpy(sm_message, message); // if this is a menu with a quit statement at the end (manually passed; not calculated), then // we need to add the right type of ending based on if it is a socket or fifo if(quit) { #ifdef ENABLE_SOCKETS if (use_sockets) { strcat(sm_message, "kill "); itoa(child_pid, child_pid_str); strcat(sm_message, child_pid_str); strcpy(child_pid_str,""); // very important because we will use it again } else { #endif // no socket strcat(sm_message, "echo 'q' > "); strcat(sm_message, fifo); #ifdef ENABLE_SOCKETS } strcat(sm_message, "\n"); #endif } #ifdef ENABLE_SOCKETS if (use_sockets) { send_sock_message(sock, sm_message); } else { #endif send_fifo_message(stream, sm_message); #ifdef ENABLE_SOCKETS } #endif return 0; } #ifdef ENABLE_SOCKETS void *send_sock_message(int socket, const char *message) { signal(SIGPIPE, SIG_IGN); if (debug >= 2) printf("Msg: %s", message); int response; if ((response=write(socket, message, strlen(message))) == -1) { printf("socket %d, msg %s\n", socket, message); perror("cannot send to socket"); //exit(EXIT_FAILURE); } if (debug >= 3) printf("response: %d\n", response); } void *send_sock_icon_message(int socket, const char *iconfile) { // msg is global buffer for messages to dump into a pipe strcpy(msg, "i "); strcat(msg, iconfile); strcat(msg, "\n"); // very important for mktrayicon send_sock_message(socket, msg); } #endif void *send_fifo_message(FILE *stream, const char *message) { if (debug >= 2) printf("Msg: %s", message); int response = fprintf(stream, message); if (debug >= 3) printf("response: %d\n", response); fflush(stream); } void *send_fifo_icon_message(FILE *stream, const char *iconfile) { // msg is global buffer for messages to dump into a pipe strcpy(msg, "i "); strcat(msg, iconfile); strcat(msg, "\n"); // very important for mktrayicon send_fifo_message(stream, msg); } void *send_icon_message(const char *iconfile, const int sock, FILE *stream) { #ifdef ENABLE_SOCKETS if (use_sockets) { send_sock_icon_message(sock, iconfile); } else { #endif send_fifo_icon_message(stream, iconfile); #ifdef ENABLE_SOCKETS } #endif } void sig_usr(int signo) { if (signo == SIGUSR1) { printf("an icon has exited!\n"); closed_icons++; }; return; } int fprint_usage(char **argv) { printf("Usage: %s [-c CONFFILE] [-d] [-h] [-s] [-n]\n", *argv); printf("Displays Capslock and Numlock status as trayicons\n"); printf("\n"); printf(" -c CONFFILE\tUse the specified config file (required)\n"); printf(" -d \tTurn debug messages on\n"); printf(" -h \tDisplay this help message\n"); printf(" -s \tUse socket instead of fifo (if compiled)\n"); printf(" -n \tNo click-to-switch Caps/Num lock\n"); printf("\n"); return 0; } int main(int argc, char **argv) { // Step 1: parse parameters int c; while ((c = getopt(argc, argv, "c:dhsn")) != -1) switch (c) { case 'c': conffile = optarg; break; case 'd': debug++; break; case 's': #ifdef ENABLE_SOCKETS use_sockets = 1; #else printf("this binary was not compiled with socket support. Aborted.\n"); exit(1); #endif break; case 'n': click_to_switch = 0; break; case 'h': return fprint_usage(argv); break; case '?': fprintf(stderr, "Unknown option: %c\n", optopt); return 1; } // must reset this for any second getopt/optint operations to proceed. This is somehow a globally-scoped variable. optind = 0; if (debug) { printf("Debug level: %d\n", debug); }; // Step 2: load config if (ini_parse(conffile, handler, &config) < 0) { printf("Error: cannot load %s\n",conffile); return 1; } if (debug) { printf("Config loaded from %s.\n", conffile); printf("caps_on=%s\n", config.caps_on); printf("caps_off=%s\n", config.caps_off); printf("num_on=%s\n", config.num_on); printf("num_off=%s\n", config.num_off); printf("sleep_us=%d\n", config.sleep_microseconds); printf("caps_fifo=%s\n", config.caps_fifo); printf("num_fifo=%s\n", config.num_fifo); } // Step 3: validate config // hardcoded min/max for microseconds, 0 and 10 seconds if (config.sleep_microseconds < 0 || config.sleep_microseconds >= 10000000) { printf("Warning: invalid sleep_microseconds: %d\n", config.sleep_microseconds); } // Step 4: initialize variables FILE *stream_N, *stream_C; 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); #ifdef ENABLE_SOCKETS if (!use_sockets) { #endif if (stat(config.caps_fifo, &st_C) != 0) mkfifo(config.caps_fifo, 0666); if (stat(config.num_fifo, &st_N) != 0) mkfifo(config.num_fifo, 0666); #ifdef ENABLE_SOCKETS } #endif // Step 5: start child proceses (tray icons) // parent1 pid = getpid(); if ((pid_C = fork()) == 0) { // parent1->child 1 strcpy(msg, config.caps_fifo); struct sigaction san_C; memset(&san_C,0,sizeof(san_C)); san_C.sa_handler = sigfun_child; sigaction(SIGTERM,&san_C,NULL); fmain(msg); kill(pid, SIGUSR1); printf("pid_C exiting...\n"); _exit(0); } else { // parent1 if ((pid_N = fork()) == 0) { // parent1->child2 strcpy(msg, config.num_fifo); struct sigaction san_N; memset(&san_N,0,sizeof(san_N)); san_N.sa_handler = sigfun_child; sigaction(SIGTERM,&san_N,NULL); fmain(msg); kill(pid, SIGUSR1); printf("pid_N exiting...\n"); _exit(0); } else { // parent1 // catch CTRL+C, useful only for fifo #ifdef ENABLE_SOCKETS if (use_sockets) { } else { #endif struct sigaction san_main; memset(&san_main,0,sizeof(san_main)); san_main.sa_handler = sigfun_main; sigaction(SIGINT,&san_main,NULL); #ifdef ENABLE_SOCKETS } #endif // Step 6 prepare icons at beginning int sockfd_C, sockfd_N; ssize_t nbytes_read, i, user_input_len; usleep(500000); // wait 0.5 seconds to let server open the socket /* Get socket. */ #ifdef ENABLE_SOCKETS if (use_sockets) { struct sockaddr_un sockaddr_C, sockaddr_N; sockfd_C = socket(AF_LOCAL, SOCK_STREAM, 0); sockfd_N = socket(AF_LOCAL, SOCK_STREAM, 0); if (sockfd_C == -1) { perror("client C socket"); exit(EXIT_FAILURE); } if (sockfd_N == -1) { perror("client N socket"); exit(EXIT_FAILURE); } /* Prepare sockaddr_C. */ sockaddr_C.sun_family = AF_LOCAL; strcpy(sockaddr_C.sun_path,config.caps_fifo); sockaddr_N.sun_family = AF_LOCAL; strcpy(sockaddr_N.sun_path,config.num_fifo); /* Do the actual connection. */ if (connect(sockfd_C, (struct sockaddr*)&sockaddr_C, sizeof(sockaddr_C)) == -1) { unlink(config.caps_fifo); printf("File: %s\n", sockaddr_C.sun_path); perror("client C connect"); return EXIT_FAILURE; } unlink(config.caps_fifo); // suggested to do after bind, but the client needs to delete the file after it connects. if (connect(sockfd_N, (struct sockaddr*)&sockaddr_N, sizeof(sockaddr_N)) == -1) { unlink(config.num_fifo); printf("File: %s\n", sockaddr_N.sun_path); perror("client N connect"); return EXIT_FAILURE; } unlink(config.num_fifo); } else { #endif // no sockets fd_C = open(config.caps_fifo, O_RDWR); stream_C = fdopen(fd_C,"w"); fd_N = open(config.num_fifo, O_RDWR); stream_N = fdopen(fd_N,"w"); #ifdef ENABLE_SOCKETS } #endif signal(SIGPIPE, SIG_IGN); // Prepare menus and initial icons usleep(5000); send_message("m quit,", sockfd_C, stream_C, pid_C, config.caps_fifo, 1); usleep(5000); if (click_to_switch) { #ifdef ENABLE_LIBXDO send_message("c CAPS\n", sockfd_C, stream_C, 0, "", 0); send_message("c NUM\n", sockfd_N, stream_N, 0, "", 0); #else send_message("c xdotool key Caps_Lock\n", sockfd_C, stream_C, -1, "", 0); send_message("c xdotool key Num_Lock\n", sockfd_N, stream_N, -1, "", 0); #endif usleep(5000); } send_message("m say hello,yad 'hello'|quit,", sockfd_N, stream_N, pid_N, config.num_fifo, 1); if (status_capslock) { send_icon_message(config.caps_on, sockfd_C, stream_C); } else { send_icon_message(config.caps_off, sockfd_C, stream_C); } if (status_numlock) { send_icon_message(config.num_on, sockfd_N, stream_N); } else { send_icon_message(config.num_off, sockfd_N, stream_N); } // still parent1 // Step 7: loop monitoring logic int status_caps_old = -1; int status_num_old = -1; //int kill_result = -1; // disabled below // call handler that counts how many child processes have quit struct sigaction san; memset(&san,0,sizeof(san)); san.sa_handler = sig_usr; sigaction(SIGUSR1,&san,NULL); while (closed_icons < 2) { // (1000000); 1 second // ( 1000); 1 millisecond // ( 100000); 0.1 seconds is a good number usleep(config.sleep_microseconds); 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) { // unfortunately, kill does not operate as expected. I expected kill_result <> 0 if the pid is already gone. //kill_result = kill(pid_C, 0); if (status_capslock) { send_icon_message(config.caps_on, sockfd_C, stream_C); usleep(5000); send_message("t capslock on\n", sockfd_C, stream_C, 0, "", 0); printf("capslock on\n"); } else { send_icon_message(config.caps_off, sockfd_C, stream_C); usleep(5000); send_message("t capslock off\n", sockfd_C, stream_C, 0, "", 0); printf("capslock off\n"); } status_caps_old = status_capslock; }; if (status_numlock != status_num_old) { if (status_numlock) { send_icon_message(config.num_on, sockfd_N, stream_N); usleep(5000); send_message("t numlock on\n", sockfd_N, stream_N, 0, "", 0); printf("numlock on\n"); } else { send_icon_message(config.num_off, sockfd_N, stream_N); usleep(5000); send_message("t numlock off\n", sockfd_N, stream_N, 0, "", 0); printf("numlock off\n"); } status_num_old = status_numlock; }; }; // end main while loop // Either CTRL+C was run twice, or both icons closed // just in case kill(pid_C,SIGTERM); kill(pid_N,SIGTERM); #ifdef ENABLE_SOCKETS if (use_sockets) { } else { #endif // important to delete these ONLY if they are fifos unlink(config.caps_fifo); unlink(config.num_fifo); #ifdef ENABLE_SOCKETS } #endif return 0; }; // end parent1 loop return 0; }; // end parent1 loop return 0; }