1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <getopt.h>
5 #include <unistd.h>
6 #include <pthread.h>
7 #include <time.h>
8 #include <stdbool.h>
9 #include <signal.h>
10
11 #include <wayland-client.h>
12 #include <wayland-egl.h>
13 #include "wlr-layer-shell-unstable-v1-client-protocol.h"
14 #include "xdg-output-unstable-v1-client-protocol.h"
15
16 #include <glad/glad.h>
17 #include <glad/glad_egl.h>
18
19 #include <mpv/client.h>
20 #include <mpv/render_gl.h>
21
22 #include <cflogprinter.h>
23
24 typedef unsigned int uint;
25
26 struct wl_state {
27 struct wl_display *display;
28 struct wl_compositor *compositor;
29 struct zwlr_layer_shell_v1 *layer_shell;
30 struct zxdg_output_manager_v1 *xdg_output_manager;
31 struct wl_list outputs; // struct display_output::link
32 char* monitor; // User selected output
33 int surface_layer;
34 bool run_display;
35 };
36
37 struct display_output {
38 uint32_t wl_name;
39 struct wl_output *wl_output;
40 struct zxdg_output_v1 *xdg_output;
41 char *name;
42 char *identifier;
43
44 struct wl_state *state;
45 struct wl_surface *surface;
46 struct zwlr_layer_surface_v1 *layer_surface;
47
48 uint32_t width, height;
49
50 struct wl_list link;
51 };
52
53 static struct wl_egl_window* egl_window;
54 static EGLDisplay *egl_display;
55 static EGLContext *egl_context;
56 static EGLSurface *egl_surface;
57
58 static mpv_handle *mpv;
59 static mpv_render_context *mpv_glcontext;
60 static char *video_path;
61 static char *opt_config_path;
62
63 static struct {
64 char **pauselist;
65 char **stoplist;
66
67 char **argv_copy;
68 char *save_info;
69
70 bool auto_pause;
71 bool auto_stop;
72
73 int is_paused;
74 bool frame_ready;
75 bool kill_render_loop;
76
77 } halt_info = {NULL, NULL, NULL, NULL, 0, 0, 0, 0, 0};
78
79 static pthread_t threads[5];
80
81 static uint SLIDESHOW_TIME = 0;
82 static bool VERBOSE = 0;
83
nop()84 static void nop() {}
85
exit_cleanup()86 static void exit_cleanup() {
87 // Cancel all threads
88 for(uint i=0; threads[i] != 0; i++) {
89 if (pthread_self() != threads[i])
90 pthread_cancel(threads[i]);
91 }
92
93 // Give mpv a chance to finish
94 halt_info.kill_render_loop = 1;
95 for (int trys=10; halt_info.kill_render_loop && trys > 0; trys--) {
96 usleep(10000);
97 }
98
99 if (mpv_glcontext)
100 mpv_render_context_free(mpv_glcontext);
101 if (mpv)
102 mpv_terminate_destroy(mpv);
103
104 if (egl_surface)
105 eglDestroySurface(egl_display, egl_surface);
106 if (egl_context)
107 eglDestroyContext(egl_display, egl_context);
108 if (egl_window)
109 wl_egl_window_destroy(egl_window);
110 }
111
exit_mpvpaper(int reason)112 static void exit_mpvpaper(int reason) {
113 if (VERBOSE)
114 cflp_info("Exiting mpvpaper");
115 exit_cleanup();
116 exit(reason);
117 }
118
exit_by_pthread()119 static void *exit_by_pthread() { exit_mpvpaper(1); pthread_exit(NULL);}
120
handle_signal(int signum)121 static void handle_signal(int signum) {
122 (void) signum;
123 // Separate thread to avoid crash
124 pthread_t thread;
125 pthread_create(&thread, NULL, exit_by_pthread, NULL);
126 }
127
128 const static struct wl_callback_listener wl_surface_frame_listener;
129
render(struct display_output * output)130 static void render(struct display_output *output) {
131 mpv_render_param render_params[] = {
132 {MPV_RENDER_PARAM_OPENGL_FBO, &(mpv_opengl_fbo){
133 .fbo = 0,
134 .w = output->width,
135 .h = output->height,
136 }},
137 // Flip rendering (needed due to flipped GL coordinate system).
138 {MPV_RENDER_PARAM_FLIP_Y, &(int){1}},
139 };
140 // Render frame
141 mpv_render_context_render(mpv_glcontext, render_params);
142
143 // Callback new frame
144 struct wl_callback *callback = wl_surface_frame(output->surface);
145 wl_callback_add_listener(callback, &wl_surface_frame_listener, output);
146
147 // Display frame
148 if (!eglSwapBuffers(egl_display, egl_surface))
149 cflp_error("Failed to swap egl buffers 0x%X", eglGetError());
150 }
151
frame_handle_done(void * data,struct wl_callback * callback,uint32_t frame_time)152 static void frame_handle_done(void *data, struct wl_callback *callback, uint32_t frame_time) {
153 (void) frame_time;
154 wl_callback_destroy(callback);
155
156 // Reset deadman switch timer
157 halt_info.frame_ready = 1;
158
159 // Sleep more while paused
160 if (halt_info.is_paused) {
161 int start_time = time(NULL);
162 while (halt_info.is_paused) {
163 if (time(NULL) - start_time >= 1)
164 break;
165 usleep(1000);
166 }
167 }
168
169 // Render next frame
170 if (!halt_info.kill_render_loop)
171 render(data);
172 else
173 halt_info.kill_render_loop = 0;
174 }
175
176 const static struct wl_callback_listener wl_surface_frame_listener = {
177 .done = frame_handle_done,
178 };
179
stop_mpvpaper()180 static void stop_mpvpaper() {
181
182 // Save video position to arg -Z
183 const char* time_pos = mpv_get_property_string(mpv, "time-pos");
184 const char* playlist_pos = mpv_get_property_string(mpv, "playlist-pos");
185
186 char save_info[30];
187 sprintf(save_info, "%s %s", time_pos, playlist_pos);
188
189 int argv_alloc_size = strlen("-Z")+1 + strlen(save_info)+1;
190 for(uint i=0; halt_info.argv_copy[i] != NULL; i++) {
191 argv_alloc_size += strlen(halt_info.argv_copy[i])+1;
192 }
193 char **argv = calloc(argv_alloc_size+1, sizeof(char));
194
195 uint i = 0;
196 for(i=0; halt_info.argv_copy[i] != NULL; i++) {
197 argv[i] = strdup(halt_info.argv_copy[i]);
198 }
199 argv[i] = "-Z";
200 argv[i+1] = save_info;
201 argv[i+2] = NULL;
202
203 // Get the "real" cwd
204 char exe_dir[1024];
205 int cut_point = readlink("/proc/self/exe", exe_dir, sizeof(exe_dir));
206 for(uint i=cut_point; i > 1; i--) {
207 if (exe_dir[i] == '/') {
208 exe_dir[i+1] = '\0';
209 break;
210 }
211 }
212
213 exit_cleanup();
214 // Start holder script
215 execv(strcat(exe_dir, "mpvpaper-holder"), argv);
216
217 cflp_error("Failed to stop mpvpaper");
218 exit(EXIT_FAILURE);
219 }
220
221 // Allow pthread_cancel while sleeping
pthread_sleep(uint time)222 static void pthread_sleep(uint time) {
223 pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
224 sleep(time);
225 pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
226 }
pthread_usleep(uint time)227 static void pthread_usleep(uint time) {
228 pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
229 usleep(time);
230 pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
231 }
232
check_watch_list(char ** list)233 static char *check_watch_list(char **list) {
234
235 char pid_name[512] = {0};
236
237 for (uint i=0; list[i] != NULL; i++) {
238 strcpy(pid_name, "pgrep ");
239 strcat(pid_name, list[i]);
240 strcat(pid_name, " > /dev/null");
241
242 // Stop if program is open
243 if (!system(pid_name)) {
244 return list[i];
245 }
246 }
247 return NULL;
248 }
249
monitor_pauselist()250 static void *monitor_pauselist() {
251 pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
252 bool is_paused = 0;
253
254 while (halt_info.pauselist) {
255 if (!halt_info.is_paused) {
256 char *app;
257 while ((app = check_watch_list(halt_info.pauselist))) {
258 if (app && !is_paused) {
259 if (VERBOSE)
260 cflp_info("Pausing for %s", app);
261 mpv_command_async(mpv, 0, (const char*[]) {"set", "pause", "yes", NULL});
262 is_paused = 1;
263 halt_info.is_paused += 1;
264 }
265 pthread_sleep(1);
266 }
267 if (is_paused) {
268 is_paused = 0;
269 halt_info.is_paused -= 1;
270 }
271 }
272 pthread_sleep(1);
273 }
274 pthread_exit(NULL);
275 }
276
monitor_stoplist()277 static void *monitor_stoplist() {
278 pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
279
280 while (halt_info.stoplist) {
281 char *app = check_watch_list(halt_info.stoplist);
282 if (app) {
283 if (VERBOSE)
284 cflp_info("Stopping for %s", app);
285 stop_mpvpaper();
286 }
287 pthread_sleep(1);
288 }
289 pthread_exit(NULL);
290 }
291
handle_auto_pause()292 static void *handle_auto_pause() {
293 pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
294
295 while (halt_info.auto_pause) {
296 if (!halt_info.is_paused) {
297 time_t start_time = time(NULL);
298 bool is_paused = 0;
299
300 // Set deadman switch timer
301 halt_info.frame_ready = 0;
302 while(!halt_info.frame_ready) {
303 if ((time(NULL) - start_time) >= 2 && !is_paused) {
304 if (VERBOSE)
305 cflp_info("Pausing because mpvpaper is hidden");
306 mpv_command_async(mpv, 0, (const char*[]) {"set", "pause", "yes", NULL});
307 is_paused = 1;
308 halt_info.is_paused += 1;
309 }
310 pthread_usleep(10000);
311 }
312 if (is_paused) {
313 is_paused = 0;
314 halt_info.is_paused -= 1;
315 }
316 }
317 pthread_sleep(1);
318 }
319 pthread_exit(NULL);
320 }
321
handle_auto_stop()322 static void *handle_auto_stop() {
323 pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
324
325 while (halt_info.auto_stop) {
326 time_t start_time = time(NULL);
327
328 // Set deadman switch timer
329 halt_info.frame_ready = 0;
330 while(!halt_info.frame_ready) {
331 if ((time(NULL) - start_time) >= 2) {
332 if (VERBOSE)
333 cflp_info("Stopping because mpvpaper is hidden");
334 stop_mpvpaper();
335 break;
336 }
337 pthread_usleep(10000);
338 }
339 pthread_sleep(1);
340 }
341 pthread_exit(NULL);
342 }
343
handle_mpv_events()344 static void *handle_mpv_events() {
345 pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
346 bool mpv_paused = 0;
347 time_t start_time = time(NULL);
348
349 while (!halt_info.kill_render_loop) {
350 if (SLIDESHOW_TIME) {
351 if ((time(NULL) - start_time) >= SLIDESHOW_TIME) {
352 mpv_command_async(mpv, 0, (const char*[]) {"playlist-next", NULL});
353 start_time = time(NULL);
354 }
355 }
356
357 mpv_event* event = mpv_wait_event(mpv, 0);
358 if (event->event_id == MPV_EVENT_SHUTDOWN || event->event_id == MPV_EVENT_IDLE)
359 exit_mpvpaper(0);
360 else if (event->event_id == MPV_EVENT_PAUSE) {
361 mpv_paused = 1;
362 // User paused
363 if (!halt_info.is_paused)
364 halt_info.is_paused += 1;
365 }
366 else if (event->event_id == MPV_EVENT_UNPAUSE) {
367 mpv_paused = 0;
368 halt_info.is_paused = 0;
369 }
370
371 if (!halt_info.is_paused && mpv_paused) {
372 mpv_command_async(mpv, 0, (const char*[]) {"set", "pause", "no", NULL});
373 }
374
375 pthread_usleep(10000);
376 }
377 pthread_exit(NULL);
378 }
379
init_threads()380 static void init_threads() {
381 uint id = 0;
382
383 pthread_create(&threads[id], NULL, handle_mpv_events, NULL);
384 id++;
385
386 // Thread for monitoring if mpvpaper is hidden
387 if (halt_info.auto_pause) {
388 pthread_create(&threads[id], NULL, handle_auto_pause, NULL);
389 id++;
390 }
391 else if (halt_info.auto_stop) {
392 pthread_create(&threads[id], NULL, handle_auto_stop, NULL);
393 id++;
394 }
395
396 // Threads for monitoring watch lists
397 if (halt_info.pauselist) {
398 pthread_create(&threads[id], NULL, monitor_pauselist, NULL);
399 id++;
400 }
401 if (halt_info.stoplist) {
402 pthread_create(&threads[id], NULL, monitor_stoplist, NULL);
403 id++;
404 }
405 }
406
set_init_mpv_options()407 static void set_init_mpv_options() {
408 // Enable user control through terminal by default
409 mpv_set_option_string(mpv, "input-default-bindings", "yes");
410 mpv_set_option_string(mpv, "input-terminal", "yes");
411 mpv_set_option_string(mpv, "terminal", "yes");
412
413 // Load user configs
414 const char *home_dir = getenv("HOME");
415 char *config_path = calloc(strlen(home_dir)+1 + 30, sizeof(char));
416 strcpy(config_path, home_dir);
417
418 char loaded_configs[50] = "";
419
420 strcpy(config_path+strlen(home_dir), "/.config/mpv/mpv.conf");
421 if (mpv_load_config_file(mpv, config_path) == 0)
422 strcat(loaded_configs, "mpv.conf ");
423 strcpy(config_path+strlen(home_dir), "/.config/mpv/input.conf");
424 if (mpv_load_config_file(mpv, config_path) == 0)
425 strcat(loaded_configs, "input.conf ");
426 strcpy(config_path+strlen(home_dir), "/.config/mpv/fonts.conf");
427 if(mpv_load_config_file(mpv, config_path) == 0)
428 strcat(loaded_configs, "fonts.conf ");
429 free(config_path);
430
431 if (VERBOSE && strcmp(loaded_configs, ""))
432 cflp_info("Loaded [ %s] user configs from \"~/.config/mpv/\"", loaded_configs);
433
434 // Convenience options passed for slideshow mode
435 if (SLIDESHOW_TIME != 0) {
436 mpv_set_option_string(mpv, "loop", "yes");
437 mpv_set_option_string(mpv, "loop-playlist", "yes");
438 }
439
440 // Set mpv_options passed
441 mpv_load_config_file(mpv, opt_config_path);
442 remove(opt_config_path);
443
444 }
445
get_proc_address_mpv(void * ctx,const char * name)446 static void *get_proc_address_mpv(void *ctx, const char *name){
447 (void) ctx;
448 return eglGetProcAddress(name);
449 }
450
init_mpv(struct display_output * output)451 static void init_mpv(struct display_output *output) {
452
453 mpv = mpv_create();
454 if (!mpv) {
455 cflp_error("Failed creating mpv context");
456 exit_mpvpaper(1);
457 }
458
459 set_init_mpv_options();
460
461 if (mpv_initialize(mpv) < 0) {
462 cflp_error("mpv init failed");
463 exit_mpvpaper(1);
464 }
465
466 // Force libmpv vo as nothing else will work
467 char *vo_option = mpv_get_property_string(mpv, "options/vo");
468 if (strcmp(vo_option, "libmpv") != 0 && strcmp(vo_option, "") != 0) {
469 cflp_warning("mpvpaper does not support any other vo than \"libmpv\"");
470 mpv_set_option_string(mpv, "vo", "libmpv");
471 }
472
473 // Have mpv render onto egl context
474 mpv_render_param params[] = {
475 {MPV_RENDER_PARAM_WL_DISPLAY, output->state->display},
476 {MPV_RENDER_PARAM_API_TYPE, MPV_RENDER_API_TYPE_OPENGL},
477 {MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &(mpv_opengl_init_params){
478 .get_proc_address = get_proc_address_mpv,
479 }},
480 };
481 if (mpv_render_context_create(&mpv_glcontext, mpv, params) < 0)
482 cflp_error("Failed to initialize mpv GL context");
483
484 // Restore video position after auto stop event
485 char* default_start = NULL;
486 if (halt_info.save_info) {
487 char time_pos[10];
488 char playlist_pos[10];
489 sscanf(halt_info.save_info, "%s %s", time_pos, playlist_pos);
490
491 // Save default start pos
492 default_start = mpv_get_property_string(mpv, "start");
493 // Restore video position
494 mpv_command_async(mpv, 0, (const char*[]) {"set", "start", time_pos, NULL});
495 // Recover playlist pos, that is if it's not shuffled...
496 mpv_command_async(mpv, 0, (const char*[]) {"set", "playlist-start", playlist_pos, NULL});
497 }
498
499 mpv_command_async(mpv, 0, (const char*[]) {"loadfile", video_path, NULL});
500
501 mpv_event* event = mpv_wait_event(mpv, 1);
502 while (event->event_id != MPV_EVENT_FILE_LOADED){
503 event = mpv_wait_event(mpv, 1);
504 }
505 if (VERBOSE)
506 cflp_info("Loaded %s", video_path);
507
508 // Return start pos to default
509 if (default_start)
510 mpv_command_async(mpv, 0, (const char*[]) {"set", "start", default_start, NULL});
511 }
512
init_egl(struct display_output * output)513 static void init_egl(struct display_output *output) {
514
515 egl_window = wl_egl_window_create(output->surface, output->width, output->height);
516 if (!egl_display) {
517 egl_display = eglGetPlatformDisplay(EGL_PLATFORM_WAYLAND_KHR, output->state->display, NULL);
518 eglInitialize(egl_display, NULL, NULL);
519 }
520 eglBindAPI(EGL_OPENGL_API);
521 const EGLint win_attrib[] = {
522 EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
523 EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
524 EGL_RED_SIZE, 8,
525 EGL_GREEN_SIZE, 8,
526 EGL_BLUE_SIZE, 8,
527 EGL_NONE
528 };
529
530 EGLConfig config;
531 EGLint config_len;
532 eglChooseConfig(egl_display, win_attrib, &config, 1, &config_len);
533
534 if (!egl_context) {
535 // Check for OpenGL compatibility for creating egl context
536 static const struct { int major, minor; } gl_versions[] = {
537 {4, 6}, {4, 5}, {4, 4}, {4, 3}, {4, 2}, {4, 1}, {4, 0},
538 {3, 3}, {3, 2}, {3, 1}, {3, 0},
539 {0, 0}
540 };
541 egl_context = NULL;
542 for (uint i = 0; gl_versions[i].major > 0; i++) {
543 const EGLint ctx_attrib[] = {
544 EGL_CONTEXT_MAJOR_VERSION, gl_versions[i].major,
545 EGL_CONTEXT_MINOR_VERSION, gl_versions[i].major,
546 EGL_NONE
547 };
548 egl_context = eglCreateContext(egl_display, config, EGL_NO_CONTEXT, ctx_attrib);
549 if (egl_context) {
550 if (VERBOSE) {
551 cflp_info("OpenGL %i.%i EGL context loaded", gl_versions[i].major, gl_versions[i].minor);
552 }
553 break;
554 }
555 }
556 if (!egl_context) {
557 cflp_error("Failed to create EGL context");
558 exit_mpvpaper(1);
559 }
560 }
561
562 egl_surface = eglCreatePlatformWindowSurface(egl_display, config, egl_window, NULL);
563 eglMakeCurrent(egl_display, egl_surface, egl_surface, egl_context);
564 eglSwapInterval(egl_display, 0);
565
566 gladLoadGLLoader((GLADloadproc) eglGetProcAddress);
567
568 glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
569 glViewport(0, 0, output->width, output->height);
570 }
571
destroy_display_output(struct display_output * output)572 static void destroy_display_output(struct display_output *output) {
573 if (!output) {
574 return;
575 }
576 wl_list_remove(&output->link);
577 if (output->layer_surface != NULL) {
578 zwlr_layer_surface_v1_destroy(output->layer_surface);
579 }
580 if (output->surface != NULL) {
581 wl_surface_destroy(output->surface);
582 }
583 if (egl_display && strcmp(output->name,output->state->monitor) == 0) {
584 eglDestroySurface(egl_display, egl_surface);
585 wl_egl_window_destroy(egl_window);
586 }
587 zxdg_output_v1_destroy(output->xdg_output);
588 wl_output_destroy(output->wl_output);
589
590 free(output->name);
591 free(output->identifier);
592 free(output);
593 }
594
layer_surface_configure(void * data,struct zwlr_layer_surface_v1 * surface,uint32_t serial,uint32_t width,uint32_t height)595 static void layer_surface_configure(void *data, struct zwlr_layer_surface_v1 *surface,
596 uint32_t serial, uint32_t width, uint32_t height) {
597 struct display_output *output = data;
598 output->width = width;
599 output->height = height;
600 zwlr_layer_surface_v1_ack_configure(surface, serial);
601
602 // Setup render loop
603 init_egl(output);
604 if (!mpv) {
605 init_mpv(output);
606 init_threads();
607 }
608
609 if (egl_display && mpv_glcontext) {
610 // Start render loop
611 render(output);
612 }
613 }
614
layer_surface_closed(void * data,struct zwlr_layer_surface_v1 * surface)615 static void layer_surface_closed(void *data, struct zwlr_layer_surface_v1 *surface) {
616 (void) surface;
617
618 struct display_output *output = data;
619 if (VERBOSE)
620 cflp_info("Destroying output %s (%s)", output->name, output->identifier);
621 destroy_display_output(output);
622 }
623
624 static const struct zwlr_layer_surface_v1_listener layer_surface_listener = {
625 .configure = layer_surface_configure,
626 .closed = layer_surface_closed,
627 };
628
create_layer_surface(struct display_output * output)629 static void create_layer_surface(struct display_output *output) {
630 output->surface = wl_compositor_create_surface(output->state->compositor);
631
632 // Empty input region
633 struct wl_region *input_region = wl_compositor_create_region(output->state->compositor);
634 wl_surface_set_input_region(output->surface, input_region);
635 wl_region_destroy(input_region);
636
637 output->layer_surface = zwlr_layer_shell_v1_get_layer_surface(
638 output->state->layer_shell, output->surface, output->wl_output,
639 output->state->surface_layer, "mpvpaper");
640
641 zwlr_layer_surface_v1_set_size(output->layer_surface, 0, 0);
642 zwlr_layer_surface_v1_set_anchor(output->layer_surface,
643 ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP |
644 ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT |
645 ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM |
646 ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT);
647 zwlr_layer_surface_v1_set_exclusive_zone(output->layer_surface, -1);
648 zwlr_layer_surface_v1_add_listener(output->layer_surface, &layer_surface_listener, output);
649 wl_surface_commit(output->surface);
650 }
651
xdg_output_handle_name(void * data,struct zxdg_output_v1 * xdg_output,const char * name)652 static void xdg_output_handle_name(void *data,
653 struct zxdg_output_v1 *xdg_output, const char *name) {
654 (void) xdg_output;
655
656 struct display_output *output = data;
657 output->name = strdup(name);
658 }
659
xdg_output_handle_description(void * data,struct zxdg_output_v1 * xdg_output,const char * description)660 static void xdg_output_handle_description(void *data,
661 struct zxdg_output_v1 *xdg_output, const char *description) {
662 (void) xdg_output;
663
664 struct display_output *output = data;
665
666 // wlroots currently sets the description to `make model serial (name)`
667 // If this changes in the future, this will need to be modified.
668 char *paren = strrchr(description, '(');
669 if (paren) {
670 size_t length = paren - description;
671 output->identifier = calloc(length, sizeof(char));
672 if (!output->identifier) {
673 cflp_warning("Failed to allocate output identifier");
674 return;
675 }
676 strncpy(output->identifier, description, length);
677 output->identifier[length - 1] = '\0';
678 }
679 }
680
xdg_output_handle_done(void * data,struct zxdg_output_v1 * xdg_output)681 static void xdg_output_handle_done(void *data, struct zxdg_output_v1 *xdg_output) {
682 (void) xdg_output;
683
684 struct display_output *output = data;
685
686 if (strcmp(output->name, output->state->monitor) == 0 && !output->layer_surface) {
687 if (VERBOSE)
688 cflp_info("Output %s (%s) selected", output->name, output->identifier);
689 create_layer_surface(output);
690 }
691 else {
692 if (VERBOSE)
693 cflp_warning("Output %s (%s) not selected", output->name, output->identifier);
694 destroy_display_output(output);
695 }
696 }
697
698 static const struct zxdg_output_v1_listener xdg_output_listener = {
699 .logical_position = nop,
700 .logical_size = nop,
701 .name = xdg_output_handle_name,
702 .description = xdg_output_handle_description,
703 .done = xdg_output_handle_done,
704 };
705
handle_global(void * data,struct wl_registry * registry,uint32_t name,const char * interface,uint32_t version)706 static void handle_global(void *data, struct wl_registry *registry,
707 uint32_t name, const char *interface, uint32_t version) {
708 (void) version;
709
710 struct wl_state *state = data;
711 if (strcmp(interface, wl_compositor_interface.name) == 0) {
712 state->compositor = wl_registry_bind(registry, name, &wl_compositor_interface, 4);
713 } else if (strcmp(interface, wl_output_interface.name) == 0) {
714 struct display_output *output = calloc(1, sizeof(struct display_output));
715 output->state = state;
716 output->wl_name = name;
717 output->wl_output = wl_registry_bind(registry, name, &wl_output_interface, 3);
718
719 wl_list_insert(&state->outputs, &output->link);
720
721 if (state->run_display) {
722 output->xdg_output = zxdg_output_manager_v1_get_xdg_output(
723 state->xdg_output_manager, output->wl_output);
724 zxdg_output_v1_add_listener(output->xdg_output, &xdg_output_listener, output);
725 }
726 } else if (strcmp(interface, zwlr_layer_shell_v1_interface.name) == 0) {
727 state->layer_shell = wl_registry_bind(registry, name,
728 &zwlr_layer_shell_v1_interface, 1);
729 } else if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) {
730 state->xdg_output_manager = wl_registry_bind(registry, name,
731 &zxdg_output_manager_v1_interface, 2);
732 }
733 }
734
handle_global_remove(void * data,struct wl_registry * registry,uint32_t name)735 static void handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) {
736 (void) registry;
737
738 struct wl_state *state = data;
739 struct display_output *output, *tmp;
740 wl_list_for_each_safe(output, tmp, &state->outputs, link) {
741 if (output->wl_name == name) {
742 cflp_info("Destroying output %s (%s)", output->name, output->identifier);
743 destroy_display_output(output);
744 break;
745 }
746 }
747 }
748
749 static const struct wl_registry_listener registry_listener = {
750 .global = handle_global,
751 .global_remove = handle_global_remove,
752 };
753
get_watch_list(char * path_name)754 static char **get_watch_list(char *path_name) {
755
756 FILE *file = fopen(path_name, "r");
757 if (file) {
758 // Get alloc size
759 fseek(file, 0L, SEEK_END);
760 char **list = calloc(ftell(file) + 1, sizeof(char));
761 rewind(file);
762
763 // Read lines
764 char app[512];
765 for (uint i=0; fscanf(file, "%s", app) == 1; i++) {
766 list[i] = strdup(app);
767 }
768
769 fclose(file);
770 return list;
771 }
772 return NULL;
773 }
774
set_watch_lists()775 static void set_watch_lists() {
776 const char *home_dir = getenv("HOME");
777
778 char *pause_path = calloc(strlen(home_dir)+1 + strlen("/.config/mpvpaper/pauselist")+1, sizeof(char));
779 strcpy(pause_path, home_dir);
780 strcat(pause_path, "/.config/mpvpaper/pauselist");
781 halt_info.pauselist = get_watch_list(pause_path);
782 free(pause_path);
783
784 char *stop_path = calloc(strlen(home_dir)+1 + strlen("/.config/mpvpaper/stoplist")+1, sizeof(char));
785 strcpy(stop_path, home_dir);
786 strcat(stop_path, "/.config/mpvpaper/stoplist");
787 halt_info.stoplist = get_watch_list(stop_path);
788 free(stop_path);
789 }
790
parse_command_line(int argc,char ** argv,struct wl_state * state)791 static void parse_command_line(int argc, char **argv, struct wl_state *state) {
792
793 static struct option long_options[] = {
794 {"help", no_argument, NULL, 'h'},
795 {"verbose", no_argument, NULL, 'v'},
796 {"fork", no_argument, NULL, 'f'},
797 {"auto-pause", no_argument, NULL, 'p'},
798 {"auto-stop", no_argument, NULL, 's'},
799 {"slideshow", required_argument, NULL, 'n'},
800 {"layer", required_argument, NULL, 'l'},
801 {"mpv-options", required_argument, NULL, 'o'},
802 {0, 0, 0, 0}
803 };
804
805 const char *usage =
806 "Usage: mpvpaper [options] <output> <url|path filename>\n"
807 "\n"
808 "Example: mpvpaper -vs -o \"no-audio loop\" DP-2 /path/to/video\n"
809 "\n"
810 "Options:\n"
811 "--help -h Displays this help message\n"
812 "--verbose -v Be more verbose\n"
813 "--fork -f Forks mpvpaper so you can close the terminal\n"
814 "--auto-pause -p Automagically* pause mpv when the wallpaper is hidden\n"
815 " This saves CPU usage, more or less, seamlessly\n"
816 "--auto-stop -s Automagically* stop mpv when the wallpaper is hidden\n"
817 " This saves CPU/RAM usage, although more abruptly\n"
818 "--slideshow -n SECS Slideshow mode plays the next video in a playlist every ? seconds\n"
819 " And passes mpv options \"loop loop-playlist\" for convenience\n"
820 "--layer -l LAYER Specifies shell surface layer to run on (background by default)\n"
821 "--mpv-options -o \"OPTIONS\" Forwards mpv options (Must be within quotes\"\")\n"
822 "\n"
823 "* See man page for more details\n"
824 ;
825
826 if(argc > 2) {
827 char *layer_name;
828 char *mpv_options = NULL;
829
830 char opt;
831 while((opt = getopt_long(argc, argv, "hvfpsn:l:o:Z:", long_options, NULL)) != -1) {
832
833 switch (opt) {
834 case 'h':
835 fprintf(stdout, "%s", usage);
836 exit(EXIT_SUCCESS);
837 case 'v':
838 VERBOSE = 1;
839 break;
840 case 'f':
841 if(fork() > 0) {
842 exit(EXIT_SUCCESS);
843 }
844 fclose(stdout);
845 fclose(stderr);
846 fclose(stdin);
847 break;
848 case 'p':
849 halt_info.auto_pause = 1;
850 halt_info.auto_stop = 0;
851 break;
852 case 's':
853 halt_info.auto_stop = 1;
854 halt_info.auto_pause = 0;
855 break;
856 case 'n':
857 SLIDESHOW_TIME = atoi(optarg);
858 if (SLIDESHOW_TIME == 0)
859 cflp_warning("0 or invalid time set for slideshow\n"
860 "Please use a positive integer");
861 break;
862 case 'l':
863 layer_name = strdup(optarg);
864 if(layer_name == NULL) {
865 state->surface_layer = ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND;
866 } else if(strcasecmp(layer_name, "top") == 0) {
867 state->surface_layer = ZWLR_LAYER_SHELL_V1_LAYER_TOP;
868 } else if(strcasecmp(layer_name, "bottom") == 0) {
869 state->surface_layer = ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM;
870 } else if(strcasecmp(layer_name, "background") == 0) {
871 state->surface_layer = ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND;
872 } else if(strcasecmp(layer_name, "overlay") == 0) {
873 state->surface_layer = ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY;
874 } else {
875 cflp_error("%s is not a shell surface layer\n"
876 "Your options are: top, bottom, background and overlay", layer_name);
877 exit(EXIT_FAILURE);
878 }
879 break;
880 case 'o':
881 mpv_options = strdup(optarg);
882 break;
883
884 case 'Z': // Hidden option to recover video pos after stopping
885 halt_info.save_info = strdup(optarg);
886 break;
887 }
888 }
889 if(optind + 1 >= argc) {
890 fprintf(stderr, "%s", usage);
891 exit(EXIT_FAILURE);
892 }
893 state->monitor = strdup(argv[optind]);
894 video_path = strdup(argv[optind+1]);
895
896 // Forward options to a tmp file so mpv can parse options
897 if(mpv_options) {
898 for (int i = 0; i < (int)strlen(mpv_options); i++) {
899 if (mpv_options[i] == ' ')
900 mpv_options[i] = '\n';
901 }
902 // Create config file name
903 opt_config_path = calloc(strlen("/tmp/mpvpaper.config")+1 + strlen(state->monitor)+1, sizeof(char));
904 strcpy(opt_config_path, "/tmp/mpvpaper");
905 strcat(opt_config_path, state->monitor);
906 strcat(opt_config_path, ".config");
907 // Put options into config file
908 FILE* file = fopen(opt_config_path, "w");
909 fputs(mpv_options, file);
910 fclose(file);
911 }
912
913 if(!system("pgrep swaybg > /dev/null")) {
914 cflp_warning("swaybg is running. This may block mpvpaper from being seen.");
915 }
916 }
917 else {
918 cflp_error("Not enough args passed");
919 fprintf(stderr, "%s", usage);
920 exit(EXIT_FAILURE);
921 }
922 }
923
main(int argc,char ** argv)924 int main(int argc, char **argv) {
925 signal(SIGINT, handle_signal);
926
927 struct wl_state state = {0};
928 wl_list_init(&state.outputs);
929
930 parse_command_line(argc, argv, &state);
931 set_watch_lists();
932
933 // Copy argv
934 int argv_alloc_size = 0;
935 for(int i=0; argv[i] != NULL; i++) {
936 argv_alloc_size += strlen(argv[i])+1;
937 }
938 halt_info.argv_copy = calloc(argv_alloc_size, sizeof(char));
939
940 int j = 0;
941 for(int i=0; i < argc; i++) {
942 if (strcmp(argv[i], "-Z") == 0) { // Remove hidden opt
943 i++; // Skip optind
944 }
945 else {
946 halt_info.argv_copy[j] = strdup(argv[i]);
947 j++;
948 }
949 }
950
951 state.display = wl_display_connect(NULL);
952 if (!state.display) {
953 cflp_error("Unable to connect to the compositor. "
954 "If your compositor is running, check or set the "
955 "WAYLAND_DISPLAY environment variable.");
956 return EXIT_FAILURE;
957 }
958
959 struct wl_registry *registry = wl_display_get_registry(state.display);
960 wl_registry_add_listener(registry, ®istry_listener, &state);
961 wl_display_roundtrip(state.display);
962 if (state.compositor == NULL || state.layer_shell == NULL ||
963 state.xdg_output_manager == NULL) {
964 cflp_error("Missing a required Wayland interface");
965 return EXIT_FAILURE;
966 }
967
968 struct display_output *output;
969 wl_list_for_each(output, &state.outputs, link) {
970 output->xdg_output = zxdg_output_manager_v1_get_xdg_output(
971 state.xdg_output_manager, output->wl_output);
972 zxdg_output_v1_add_listener(output->xdg_output,
973 &xdg_output_listener, output);
974 }
975
976 // Check outputs
977 wl_display_roundtrip(state.display);
978 if (wl_list_empty(&state.outputs)) {
979 cflp_error(":/ sorry about this but we can't seem to find any output.");
980 return EXIT_FAILURE;
981 }
982
983 state.run_display = 1;
984 while (wl_display_dispatch(state.display) != -1) {
985 // NOP
986 }
987
988 struct display_output *tmp_output;
989 wl_list_for_each_safe(output, tmp_output, &state.outputs, link) {
990 destroy_display_output(output);
991 }
992
993 return EXIT_SUCCESS;
994 }
995