1 #include <arcan_shmif.h>
2 #include <arcan_tui.h>
3 #include <assert.h>
4 #include <spawn.h>
5 #include <pthread.h>
6 #include <sys/types.h>
7 #include <sys/wait.h>
8 #include <signal.h>
9 #include <errno.h>
10
11 #define NANOSVG_IMPLEMENTATION
12 #define NANOSVGRAST_IMPLEMENTATION
13 #define NANOSVG_ALL_COLOR_KEYWORDS
14 #define NSVG_RGB(r, g, b)(SHMIF_RGBA(r, g, b, 0x00))
15 #include "nanosvg.h"
16 #include "nanosvgrast.h"
17
18 #include "parser.h"
19
20 struct trayicon_state {
21 /* icon sources */
22 const char* icon_normal;
23 const char* icon_pressed;
24
25 /* render controls */
26 float density;
27 bool dirty;
28 bool pressed;
29
30 /* child process control */
31 char* bin;
32 char** argv;
33 char** envv;
34 pid_t client_pid;
35 };
36
37 /* if the icon state is missing or broken, use a solid color to indicate that */
draw_fallback(struct arcan_shmif_cont * C,struct trayicon_state * trayicon)38 static void draw_fallback(
39 struct arcan_shmif_cont* C, struct trayicon_state* trayicon)
40 {
41 shmif_pixel col = trayicon->pressed ?
42 SHMIF_RGBA(0xff, 0x00, 0x00, 0xff) : SHMIF_RGBA(0x00, 0xff, 0x00, 0xff);
43 for (size_t i = 0; i < C->pitch * C->h; i++)
44 C->vidp[i] = col;
45 trayicon->dirty = true;
46 }
47
render_to(struct arcan_shmif_cont * C,struct trayicon_state * trayicon)48 static void render_to(
49 struct arcan_shmif_cont* C, struct trayicon_state* trayicon)
50 {
51 FILE* src = fopen(trayicon->pressed ?
52 trayicon->icon_pressed : trayicon->icon_normal, "r");
53
54 if (!src)
55 return draw_fallback(C, trayicon);
56
57 /* happens rarely enough that in-memory caching is just a hazzle, might
58 * as well support icon changing at runtime-, nsvgParseFromFile will fclose() */
59 NSVGimage* image = nsvgParseFromFile(src, "px", trayicon->density);
60 if (!image)
61 return draw_fallback(C, trayicon);
62
63 NSVGrasterizer* rast = nsvgCreateRasterizer();
64 if (!rast){
65 nsvgDelete(image);
66 return draw_fallback(C, trayicon);
67 }
68
69 float scalew = C->w / image->width;
70 float scaleh = C->h / image->height;
71 float scale = scalew < scaleh ? scalew : scaleh;
72
73 nsvgRasterize(rast, image, 0, 0, scale, C->vidb, C->w, C->h, C->stride);
74 nsvgDelete(image);
75 nsvgDeleteRasterizer(rast);
76 trayicon->dirty = true;
77 }
78
force_kill(pid_t pid)79 static void force_kill(pid_t pid)
80 {
81 kill(pid, SIGKILL);
82 int wstatus;
83 while (pid != waitpid(pid, &wstatus, 0) && errno != EINVAL){}
84 }
85
86 struct killarg {
87 pid_t pid;
88 int timeout;
89 };
90
kill_thread(void * T)91 static void* kill_thread(void* T)
92 {
93 struct killarg* karg = T;
94
95 free(karg);
96 return NULL;
97 }
98
toggle_client(struct arcan_shmif_cont * C,struct trayicon_state * trayicon)99 static void toggle_client(
100 struct arcan_shmif_cont* C, struct trayicon_state* trayicon)
101 {
102 /* If we don't have a client, spawn it, if we have one, first killing it softly
103 * with this song - it is a bit naive dealing with pthread creation failures as
104 * it just switches to SIGKILL and wait */
105 if (trayicon->client_pid != -1){
106 pthread_t pth;
107 pthread_attr_t attr;
108 pthread_attr_init(&attr);
109 pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
110 struct killarg* karg = malloc(sizeof(struct killarg));
111 if (!karg)
112 force_kill(trayicon->client_pid);
113 else {
114 *karg = (struct killarg){
115 .pid = trayicon->client_pid
116 };
117 if (-1 == pthread_create(&pth, &attr, kill_thread, karg)){
118 free(karg);
119 force_kill(trayicon->client_pid);
120 }
121 }
122 trayicon->client_pid = -1;
123 trayicon->pressed = false;
124 }
125 else {
126 arcan_shmif_enqueue(C, &(struct arcan_event){
127 .ext.kind = ARCAN_EVENT(SEGREQ),
128 .ext.segreq.kind = SEGID_HANDOVER
129 });
130 trayicon->pressed = true;
131 }
132
133 trayicon->dirty = true;
134 }
135
do_event(struct arcan_shmif_cont * C,struct arcan_event * ev,struct trayicon_state * trayicon)136 static void do_event(struct arcan_shmif_cont* C,
137 struct arcan_event* ev, struct trayicon_state* trayicon)
138 {
139 if (ev->category != EVENT_IO && ev->category != EVENT_TARGET)
140 return;
141
142 /* only care about the initial press */
143 if (ev->category == EVENT_IO){
144 if (ev->io.kind != EVENT_IO_BUTTON || !ev->io.input.digital.active)
145 return;
146
147 if (strcmp(ev->io.label, "ACTIVATE") == 0 ||
148 ev->io.devkind == EVENT_IDEVKIND_MOUSE){
149 toggle_client(C, trayicon);
150 }
151
152 return;
153 }
154
155 /* category == EVENT_TARGET */
156 switch (ev->tgt.kind){
157 case TARGET_COMMAND_DISPLAYHINT:{
158 size_t w = ev->tgt.ioevs[0].uiv;
159 size_t h = ev->tgt.ioevs[1].uiv;
160 if (w && h && (w != C->w || h != C->h)){
161 arcan_shmif_resize(C, w, h);
162 render_to(C, trayicon);
163 }
164 }
165 break;
166 /* got the reply to our pending request, time to handover */
167 case TARGET_COMMAND_NEWSEGMENT:{
168 trayicon->client_pid = arcan_shmif_handover_exec(C, *ev,
169 trayicon->bin, trayicon->argv, trayicon->envv, false);
170 if (-1 != trayicon->client_pid){
171 render_to(C, trayicon);
172 }
173 }
174 break;
175 /* server-side state somehow lost, kill the client if we
176 * have one, submit a frame update regardless */
177 case TARGET_COMMAND_RESET:
178 if (-1 != trayicon->client_pid){
179 toggle_client(C, trayicon);
180 }
181 trayicon->dirty = true;
182 break;
183
184 /* our CLOCKREQ timer will trigger this. */
185 case TARGET_COMMAND_STEPFRAME:
186 /* did the client exit without us trying to get rid of it? */
187 if (-1 != trayicon->client_pid){
188 if (trayicon->client_pid == waitpid(
189 trayicon->client_pid, NULL, WNOHANG)){
190 trayicon->client_pid = -1;
191 trayicon->pressed = false;
192 trayicon->dirty = true;
193 render_to(C, trayicon);
194 }
195 }
196 break;
197 default:
198 break;
199 }
200 }
201
event_loop(struct arcan_shmif_cont * C,struct trayicon_state * trayicon)202 static void event_loop(
203 struct arcan_shmif_cont* C, struct trayicon_state* trayicon)
204 {
205 struct arcan_event ev;
206
207 while(arcan_shmif_wait(C, &ev)){
208
209 /* got one event, might be more in store so flush them out */
210 do_event(C, &ev, trayicon);
211 while (arcan_shmif_poll(C, &ev) > 0)
212 do_event(C, &ev, trayicon);
213
214 /* and update if something actually changed */
215 if (trayicon->dirty){
216 arcan_shmif_signal(C, SHMIF_SIGVID);
217 }
218 }
219 }
220
show_args(const char * args,int rv)221 static int show_args(const char* args, int rv)
222 {
223 fprintf(stderr, "Usage:\n"
224 "\t icon-launch mode: icon.svg icon-active.svg exec-file [args]\n"
225 "\t stdin-tray item mode: --stdin [-w, --width n_cells]\n");
226 return rv;
227 }
228
229 extern char** environ;
230
render_buffer(struct tui_context * tui,struct parser_data * data,bool fixed)231 static void render_buffer(
232 struct tui_context* tui, struct parser_data* data, bool fixed)
233 {
234 /* get number of columns, do we fit? */
235 size_t rows, cols;
236 arcan_tui_dimensions(tui, &rows, &cols);
237 size_t start_col = 0;
238
239 if (!fixed){
240
241 /* if we don't, request that we get larger */
242 if (cols < data->buffer_used || cols > data->buffer_used + 1){
243 arcan_tui_wndhint(tui, NULL, (struct tui_constraints){
244 .min_rows = 1, .max_rows = 1,
245 .min_cols = 1, .max_cols = data->buffer_used
246 });
247 }
248
249 /* or apply alignment */
250 else {
251 if (data->icon.align == 0){
252 start_col = (cols - data->buffer_used) >> 1;
253 }
254 else if (data->icon.align == 1){
255 start_col = cols - data->buffer_used;
256 }
257 }
258
259 /* the other option would be to ticker-tape like scroll using the clock
260 * from tick and step the starting offset, but wait with that for a bit */
261 }
262
263 arcan_tui_erase_screen(tui, false);
264
265 /* try to hint the size of the buffer otherwise */
266 /* best effort draw for the time being */
267 /* position cusor based on alignment */
268 arcan_tui_move_to(tui, start_col, 0);
269 for (size_t i = 0; i < data->buffer_used; i++){
270 arcan_tui_write(tui, data->buffer[i].ch, &data->buffer[i].attr);
271 }
272
273 /* normal loop will take care of the rest */
274 }
275
on_resized(struct tui_context * tui,size_t neww,size_t newh,size_t cols,size_t rows,void * tag)276 static void on_resized(struct tui_context* tui,
277 size_t neww, size_t newh, size_t cols, size_t rows, void* tag)
278 {
279 struct parser_data* data = tag;
280 render_buffer(tui, data, true);
281 }
282
stdin_tui(const char * name,size_t w)283 static int stdin_tui(const char* name, size_t w)
284 {
285 arcan_tui_conn* conn = arcan_tui_open_display(name, "");
286 if (!conn){
287 fprintf(stderr, "Couldn't connect to arcan, check ARCAN_CONNPATH\n");
288 return EXIT_FAILURE;
289 }
290
291 /* shmif will give us a non-blocking descriptor this way */
292 int src = arcan_shmif_dupfd(STDIN_FILENO, -1, false);
293 if (-1 == src){
294 fprintf(stderr, "Couldn't create unblocking descriptor\n");
295 return EXIT_FAILURE;
296 }
297
298 struct tui_cell buffer[256];
299
300 struct parser_data tag = {
301 .buffer = buffer,
302 .buffer_count = 256,
303 .icon = {
304 .align = -1
305 }
306 };
307
308 struct tui_cbcfg cbcfg = {
309 /* .input_label = on_label, */
310 .resized = on_resized,
311 .tag = &tag
312 };
313
314 struct tui_context* tui = arcan_tui_setup(conn, NULL, &cbcfg, (sizeof(cbcfg)));
315 if (!tui){
316 fprintf(stderr, "Couldn't connect to tray (check ARCAN_CONNPATH)");
317 return EXIT_FAILURE;
318 }
319
320 tag.icon.attr = arcan_tui_defcattr(tui, TUI_COL_TEXT);
321 arcan_tui_set_flags(tui, TUI_HIDE_CURSOR);
322
323 /* this will forward our desired constraints and attempt a resize
324 * once, resized event will be triggered regardless of the event */
325 if (w){
326 arcan_tui_wndhint(tui, NULL, (struct tui_constraints){
327 .min_rows = 1, .max_rows = 1,
328 .min_cols = 1, .max_cols = w
329 });
330 }
331
332 char inbuf[256];
333 uint8_t inbuf_ofs = 0;
334 size_t n_fd = 1;
335
336 #ifdef TESTING
337 parse_lemon(tui, &tag, "Hi there");
338 render_buffer(tui, &tag, w != 0);
339 #endif
340
341 while (1){
342 struct tui_process_res res = arcan_tui_process(&tui, 1, &src, n_fd, -1);
343 if (res.errc != TUI_ERRC_OK)
344 break;
345
346 if (-1 == arcan_tui_refresh(tui) && errno == EINVAL)
347 break;
348
349 if (res.bad){
350 break;
351 }
352
353 /* 256 character crop- limited non-blocking fgets */
354 ssize_t nr;
355 while ((nr = read(src, &inbuf[inbuf_ofs], 1)) > 0){
356 if (inbuf[inbuf_ofs] == '\n' || inbuf_ofs == 255){
357 inbuf[inbuf_ofs] = '\0';
358 parse_lemon(tui, &tag, inbuf);
359 render_buffer(tui, &tag, w != 0);
360 inbuf_ofs = 0;
361 }
362 else
363 inbuf_ofs++;
364 }
365 }
366
367 arcan_tui_destroy(tui, NULL);
368 return EXIT_SUCCESS;
369 }
370
main(int argc,char ** argv)371 int main(int argc, char** argv)
372 {
373 if (argc <= 1)
374 return show_args("", EXIT_FAILURE);
375
376 if (strcmp(argv[1], "-") == 0 || strcmp(argv[1], "--stdin") == 0){
377 size_t w = 0;
378
379 if (argc == 4 && (strcmp(argv[2], "-w") == 0 || strcmp(argv[2], "--width") == 0)){
380 w = strtoul(argv[3], NULL, 10);
381 }
382 return stdin_tui("button", w);
383 }
384
385 if (argc <= 2)
386 return show_args("", EXIT_FAILURE);
387
388 /* open the connection */
389 struct arg_arr* args;
390 struct arcan_shmif_cont conn =
391 arcan_shmif_open(SEGID_ICON, SHMIF_ACQUIRE_FATALFAIL, &args);
392
393 /* get the display configuration */
394 struct arcan_shmif_initial* cfg;
395 size_t icfg = arcan_shmif_initial(&conn, &cfg);
396 assert(icfg == sizeof(struct arcan_shmif_initial));
397
398 /* rendering- options, convert ppcm to dpi */
399 struct trayicon_state state = {
400 .density = cfg->density * 0.393700787f,
401 .bin = argv[3],
402 .argv = &argv[4],
403 .client_pid = -1,
404 .envv = environ,
405 .icon_normal = argv[1],
406 .icon_pressed = argv[2]
407 };
408
409 /* register our keybinding */
410 arcan_shmif_enqueue(&conn, &(struct arcan_event){
411 .ext.kind = ARCAN_EVENT(LABELHINT),
412 .ext.labelhint = {
413 .idatatype = EVENT_IDATATYPE_DIGITAL,
414 .label = "ACTIVATE",
415 .descr = "tooltip description goes here"
416 /* we can suggest a hotkey like mapping here as well with the
417 * modifiers and initial fields and data from arcan_tuisym.h */
418 }
419 });
420
421 /* request a wakeup timer */
422 arcan_shmif_enqueue(&conn, &(struct arcan_event){
423 .ext.kind = ARCAN_EVENT(CLOCKREQ),
424 .ext.clock.rate = 5
425 });
426
427 /* update immediately so we become visible */
428 render_to(&conn, &state);
429 arcan_shmif_signal(&conn, SHMIF_SIGVID);
430 event_loop(&conn, &state);
431
432 return EXIT_SUCCESS;
433 }
434