/** * @brief Toast notification daemon. * @file apps/toastd.c * * Provides an endpoint for applications to post notifications * which are displayed in pop-up "toasts" in the upper-right * corner of the screen without stealing focus. * * @copyright * This file is part of ToaruOS and is released under the terms * of the NCSA / University of Illinois License - see LICENSE.md * Copyright (C) 2021 K. Lange */ #include #include #include #include #include #include #include #include #include #include #include typedef struct JSON_Value JSON_Value; static yutani_t * yctx; static FILE * pex_endpoint = NULL; static sprite_t background_sprite; static list_t * windows = NULL; static list_t * garbage = NULL; struct ToastNotification { yutani_window_t * window; struct timespec created; int duration; }; #define PAD_RIGHT 10 #define PAD_TOP 48 static void handle_msg(JSON_Value * msg) { if (msg->type != JSON_TYPE_OBJECT) { fprintf(stderr, "expected an object, but json value was of type %d\n", msg->type); return; } JSON_Value * msg_body = JSON_KEY(msg, "body"); if (!msg_body) { fprintf(stderr, "missing 'body'\n"); return; } if (msg_body->type != JSON_TYPE_STRING) { fprintf(stderr, "'body' should have been a string, but got type %d instead\n", msg_body->type); return; } /* At this point, we're going to show something, at least... */ yutani_window_t * win = yutani_window_create_flags(yctx, background_sprite.width, background_sprite.height, YUTANI_WINDOW_FLAG_NO_STEAL_FOCUS | YUTANI_WINDOW_FLAG_ALT_ANIMATION); yutani_set_stack(yctx, win, YUTANI_ZORDER_OVERLAY); /* TODO: We need to figure out how to place this... */ yutani_window_move(yctx, win, yctx->display_width - background_sprite.width - PAD_RIGHT, PAD_TOP + background_sprite.height * windows->length); struct ToastNotification * notification = malloc(sizeof(struct ToastNotification)); notification->window = win; clock_gettime(CLOCK_MONOTONIC, ¬ification->created); list_insert(windows, notification); JSON_Value * msg_duration = JSON_KEY(msg,"duration"); if (msg_duration && msg_duration->type == JSON_TYPE_NUMBER) { notification->duration = msg_duration->number; } else { notification->duration = 5; } /* Establish the rendering context for this window, we'll only need it for a bit. * We won't even both double buffering... */ gfx_context_t * ctx = init_graphics_yutani(win); draw_fill(ctx, rgba(0,0,0,0)); draw_sprite(ctx, &background_sprite, 0, 0); int textOffset = 0; /* Does it have an icon? */ JSON_Value * msg_icon = JSON_KEY(msg, "icon"); if (msg_icon && msg_icon->type == JSON_TYPE_STRING) { /* Just ignore the icon if it's not a string... */ sprite_t myIcon; if (!load_sprite(&myIcon, msg_icon->string)) { /* Is this a reasonable icon to display? */ if (myIcon.width < 100) { textOffset = myIcon.width + 8; /* Sounds like a fine padding... */ draw_sprite(ctx, &myIcon, 10, (background_sprite.height - myIcon.height) / 2); } free(myIcon.bitmap); } } int height = markup_string_height(msg_body->string); markup_draw_string(ctx, 10 + textOffset, (ctx->height - height) / 2, msg_body->string, rgb(255,255,255)); yutani_flip(yctx, win); } int main(int argc, char * argv[]) { /* Make sure we were actually expecting to be run... */ if (argc < 2 || strcmp(argv[1],"--really")) { fprintf(stderr, "%s: Toast notification daemon\n" "\n" " Displays popup notifications from other\n" " applications in the corner of the screen.\n" " You probably don't want to run this directly - it is\n" " started automatically by the session manager.\n", argv[0]); return 1; } /* Daemonize... */ if (!fork()) { /* Connect to display server... */ yctx = yutani_init(); if (!yctx) { fprintf(stderr, "%s: Failed to connect to compositor.\n", argv[0]); return 1; } /* Open pex endpoint to receive notifications... */ pex_endpoint = pex_bind("toast"); if (!pex_endpoint) { fprintf(stderr, "%s: Failed to establish socket.\n", argv[0]); return 1; } /* Set up our text rendering and sprite contexts... */ markup_text_init(); load_sprite(&background_sprite, "/usr/share/ttk/toast/default.png"); windows = list_create(); garbage = list_create(); int should_exit = 0; while (!should_exit) { int fds[2] = {fileno(yctx->sock),fileno(pex_endpoint)}; int index = fswait2(2,fds,windows->length ? 20 : -1); if (index == 0) { yutani_msg_t * m = yutani_poll(yctx); while (m) { switch (m->type) { case YUTANI_MSG_SESSION_END: should_exit = 1; break; default: break; } free(m); m = yutani_poll_async(yctx); } } else if (index == 1) { pex_packet_t * p = calloc(PACKET_SIZE, 1); pex_listen(pex_endpoint, p); JSON_Value * msg = json_parse((char*)p->data); if (msg) { handle_msg(msg); json_free(msg); } free(p); } if (windows->length) { /* Check all the existing toasts for expired ones... */ struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); foreach(node, windows) { struct ToastNotification * notification = node->value; /* How long has it been since this notification was created? */ struct timespec diff; diff.tv_sec = now.tv_sec - notification->created.tv_sec; diff.tv_nsec = now.tv_nsec - notification->created.tv_nsec; if (diff.tv_nsec < 0) { diff.tv_sec--; diff.tv_nsec += 1000000000L; } if (diff.tv_sec >= notification->duration) { if (notification->window) { yutani_close(yctx, notification->window); notification->window = NULL; } if (diff.tv_sec >= notification->duration + 1) { list_insert(garbage, node); } } } /* Expunge garbage */ if (garbage->length) { while (garbage->length) { node_t * n = list_pop(garbage); node_t * node = n->value; free(n); list_delete(windows, node); free(node->value); free(node); } } /* Figure out if we need to move anything */ if (index == 2) { int index = 0; foreach(node, windows) { struct ToastNotification * notification = node->value; if (notification->window && notification->window->y > PAD_TOP + background_sprite.height * index) { yutani_window_move(yctx, notification->window, notification->window->x, notification->window->y - 4); } index++; } sched_yield(); } } } } return 0; }