/* * texec.c - Minimal X11 tray applet with mandatory PNG icon (32x32) * * Copyright 2025 Johannes Findeisen * Licensed under the terms of the Apache-2.0 license. * https://opensource.org/license/apache-2-0 * * Compile: * gcc -o texec texec.c -lX11 -lpng * * Usage: * ./texec "" * * Left-click: execute command * Right-click: quit applet */ #include #include #include #include #include #include #include #include #include #define WIDTH 24 #define HEIGHT 24 static Display *dpy; static int screen; static Window icon_win; static char *x_command; void execute_command() { if (!x_command) return; pid_t pid = fork(); if (pid == 0) { setsid(); execl("/bin/sh", "sh", "-c", x_command, (char *)NULL); _exit(127); } } /* Minimal PNG loader into XImage */ XImage *load_png(const char *filename) { FILE *fp = fopen(filename, "rb"); if (!fp) return NULL; png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING,NULL,NULL,NULL); png_infop info = png_create_info_struct(png); if (setjmp(png_jmpbuf(png))) return NULL; png_init_io(png, fp); png_read_info(png, info); int w = png_get_image_width(png, info); int h = png_get_image_height(png, info); png_byte color_type = png_get_color_type(png, info); png_byte bit_depth = png_get_bit_depth(png, info); if (bit_depth == 16) png_set_strip_16(png); if (color_type == PNG_COLOR_TYPE_PALETTE) png_set_palette_to_rgb(png); if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) png_set_expand_gray_1_2_4_to_8(png); if (png_get_valid(png, info, PNG_INFO_tRNS)) png_set_tRNS_to_alpha(png); png_set_filler(png, 0xFF, PNG_FILLER_AFTER); png_read_update_info(png, info); png_bytep *rows = (png_bytep*)malloc(sizeof(png_bytep) * h); for (int y=0; ywidth, img->height); XDestroyImage(img); XFreeGC(dpy,gc); } int main(int argc, char *argv[]) { if (argc < 3) { fprintf(stderr,"Usage: %s \n",argv[0]); return 1; } x_command = argv[1]; char *icon_path = argv[2]; signal(SIGCHLD, SIG_IGN); dpy = XOpenDisplay(NULL); if (!dpy) { fprintf(stderr,"Cannot open display\n"); return 1; } screen = DefaultScreen(dpy); Atom _NET_SYSTEM_TRAY_S0 = XInternAtom(dpy, "_NET_SYSTEM_TRAY_S0", False); Window tray_win = XGetSelectionOwner(dpy, _NET_SYSTEM_TRAY_S0); if (tray_win == None) { fprintf(stderr,"No system tray found\n"); return 1; } icon_win = XCreateSimpleWindow(dpy, DefaultRootWindow(dpy), 0,0, WIDTH, HEIGHT, 1, BlackPixel(dpy,screen), WhitePixel(dpy,screen)); XSelectInput(dpy, icon_win, ExposureMask | ButtonPressMask); // Send dock request XEvent ev; memset(&ev, 0, sizeof(ev)); ev.xclient.type = ClientMessage; ev.xclient.window = icon_win; ev.xclient.message_type = XInternAtom(dpy,"_NET_SYSTEM_TRAY_OPCODE",False); ev.xclient.format = 32; ev.xclient.data.l[0] = CurrentTime; ev.xclient.data.l[1] = 0; // SYSTEM_TRAY_REQUEST_DOCK ev.xclient.data.l[2] = icon_win; XSendEvent(dpy, tray_win, False, NoEventMask, &ev); XMapWindow(dpy, icon_win); draw_icon(icon_win, icon_path); XEvent xev; while (1) { XNextEvent(dpy, &xev); if (xev.xany.window == icon_win) { if (xev.type == Expose) draw_icon(icon_win, icon_path); else if (xev.type == ButtonPress) { if (xev.xbutton.button == 1) { execute_command(); // left-click } else if (xev.xbutton.button == 3) { break; // right-click: quit } } } } XDestroyWindow(dpy, icon_win); XCloseDisplay(dpy); return 0; }