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