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, &registry_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