chiark / gitweb /
Mark `help' and `version' functions as not returning.
[disorder] / disobedience / disobedience.c
1 /*
2  * This file is part of DisOrder.
3  * Copyright (C) 2006-2009 Richard Kettlewell
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  * 
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 /** @file disobedience/disobedience.c
19  * @brief Main Disobedience program
20  */
21
22 #include "disobedience.h"
23 #include "regexp.h"
24 #include "version.h"
25
26 #include <getopt.h>
27 #include <locale.h>
28 #include <gcrypt.h>
29
30 /* Apologies for the numerous de-consting casts, but GLib et al do not seem to
31  * have heard of const. */
32
33 /* Variables --------------------------------------------------------------- */
34
35 /** @brief Event loop */
36 GMainLoop *mainloop;
37
38 /** @brief Top-level window */
39 GtkWidget *toplevel;
40
41 /** @brief Label for progress indicator */
42 GtkWidget *report_label;
43
44 /** @brief Main tab group */
45 GtkWidget *tabs;
46
47 /** @brief Mini-mode widget for playing track */
48 GtkWidget *playing_mini;
49
50 /** @brief Main client */
51 disorder_eclient *client;
52
53 /** @brief Log client */
54 disorder_eclient *logclient;
55
56 /** @brief Last reported state
57  *
58  * This is updated by log_state().
59  */
60 unsigned long last_state;
61
62 /** @brief True if some track is playing
63  *
64  * This ought to be removed in favour of last_state & DISORDER_PLAYING
65  */
66 int playing;
67
68 /** @brief Left channel volume */
69 int volume_l;
70
71 /** @brief Right channel volume */
72 int volume_r;
73
74 double goesupto = 10;                   /* volume upper bound */
75
76 /** @brief True if a NOP is in flight */
77 static int nop_in_flight;
78
79 /** @brief True if an rtp-address command is in flight */
80 static int rtp_address_in_flight;
81
82 /** @brief True if a rights lookup is in flight */
83 static int rights_lookup_in_flight;
84
85 /** @brief Current rights bitmap */
86 rights_type last_rights;
87
88 /** @brief True if RTP play is available
89  *
90  * This is a bit of a bodge...
91  */
92 int rtp_supported;
93
94 /** @brief True if RTP play is enabled */
95 int rtp_is_running;
96
97 /** @brief Server version */
98 const char *server_version;
99
100 /** @brief Parsed server version */
101 long server_version_bytes;
102
103 static GtkWidget *queue;
104
105 static GtkWidget *notebook_box;
106
107 static void check_rtp_address(const char *event,
108                               void *eventdata,
109                               void *callbackdata);
110
111 /* Window creation --------------------------------------------------------- */
112
113 /* Note that all the client operations kicked off from here will only complete
114  * after we've entered the event loop. */
115
116 /** @brief Called when main window is deleted
117  *
118  * Terminates the program.
119  */
120 static gboolean delete_event(GtkWidget attribute((unused)) *widget,
121                              GdkEvent attribute((unused)) *event,
122                              gpointer attribute((unused)) data) {
123   D(("delete_event"));
124   exit(0);                              /* die immediately */
125 }
126
127 /** @brief Called when the current tab is switched
128  *
129  * Updates the menu settings to correspond to the new page.
130  */
131 static void tab_switched(GtkNotebook *notebook,
132                          GtkNotebookPage attribute((unused)) *page,
133                          guint page_num,
134                          gpointer attribute((unused)) user_data) {
135   GtkWidget *const tab = gtk_notebook_get_nth_page(notebook, page_num);
136   const struct tabtype *const t = g_object_get_data(G_OBJECT(tab), "type");
137   assert(t != 0);
138   if(t->selected)
139     t->selected();
140 }
141
142 /** @brief Create the report box */
143 static GtkWidget *report_box(void) {
144   GtkWidget *vbox = gtk_vbox_new(FALSE, 0);
145
146   report_label = gtk_label_new("");
147   gtk_label_set_ellipsize(GTK_LABEL(report_label), PANGO_ELLIPSIZE_END);
148   gtk_misc_set_alignment(GTK_MISC(report_label), 0, 0);
149   gtk_container_add(GTK_CONTAINER(vbox), gtk_hseparator_new());
150   gtk_container_add(GTK_CONTAINER(vbox), report_label);
151   return vbox;
152 }
153
154 /** @brief Create and populate the main tab group */
155 static GtkWidget *notebook(void) {
156   tabs = gtk_notebook_new();
157   /* The current tab is _NORMAL, the rest are _ACTIVE, which is bizarre but
158    * produces not too dreadful appearance */
159   gtk_widget_set_style(tabs, tool_style);
160   g_signal_connect(tabs, "switch-page", G_CALLBACK(tab_switched), 0);
161   gtk_notebook_append_page(GTK_NOTEBOOK(tabs), queue = queue_widget(),
162                            gtk_label_new("Queue"));
163   gtk_notebook_append_page(GTK_NOTEBOOK(tabs), recent_widget(),
164                            gtk_label_new("Recent"));
165   gtk_notebook_append_page(GTK_NOTEBOOK(tabs), choose_widget(),
166                            gtk_label_new("Choose"));
167   gtk_notebook_append_page(GTK_NOTEBOOK(tabs), added_widget(),
168                            gtk_label_new("Added"));
169   return tabs;
170 }
171
172 /* Tracking of window sizes */
173 static int toplevel_width = 640, toplevel_height = 480;
174 static int mini_width = 480, mini_height = 140;
175 static struct timeval last_mode_switch;
176
177 static void main_minimode(const char attribute((unused)) *event,
178                           void attribute((unused)) *evendata,
179                           void attribute((unused)) *callbackdata) {
180   if(full_mode) {
181     gtk_window_resize(GTK_WINDOW(toplevel), toplevel_width, toplevel_height);
182     gtk_widget_show(tabs);
183     gtk_widget_hide(playing_mini);
184     /* Show the queue (bit confusing otherwise!) */
185     gtk_notebook_set_current_page(GTK_NOTEBOOK(tabs), 0);
186   } else {
187     gtk_window_resize(GTK_WINDOW(toplevel), mini_width, mini_height);
188     gtk_widget_hide(tabs);
189     gtk_widget_show(playing_mini);
190   }
191   xgettimeofday(&last_mode_switch, NULL);
192 }
193
194 /* Called when the window size is allocate */
195 static void toplevel_size_allocate(GtkWidget attribute((unused)) *w,
196                                    GtkAllocation *a,
197                                    gpointer attribute((unused)) user_data) {
198   struct timeval now;
199   xgettimeofday(&now, NULL);
200   if(tvdouble(tvsub(now, last_mode_switch)) < 0.5) {
201     /* Suppress size-allocate signals that are within half a second of a mode
202      * switch: they are quite likely to be the result of re-arranging widgets
203      * within the old size, not the application of the new size.  Yes, this is
204      * a disgusting hack! */
205     return;                             /* OMG too soon! */
206   }
207   if(full_mode) {
208     toplevel_width = a->width;
209     toplevel_height = a->height;
210   } else {
211     mini_width = a->width;
212     mini_height = a->height;
213   }
214 }
215
216 /* Periodically check the toplevel's size
217  * (the hack in toplevel_size_allocate() means we could in principle
218  * miss a user-initiated resize)
219  */
220 static void check_toplevel_size(const char attribute((unused)) *event,
221                                 void attribute((unused)) *evendata,
222                                 void attribute((unused)) *callbackdata) {
223   GtkAllocation a;
224   gtk_window_get_size(GTK_WINDOW(toplevel), &a.width, &a.height);
225   toplevel_size_allocate(NULL, &a, NULL);
226 }
227
228 static void hack_window_title(const char attribute((unused)) *event,
229                               void attribute((unused)) *eventdata,
230                               void attribute((unused)) *callbackdata) {
231   char *p;
232   const char *note;
233   static const char *last_note = 0;
234
235   if(!(last_state & DISORDER_CONNECTED))
236     note = "(disconnected)";
237   else if(last_state & DISORDER_TRACK_PAUSED)
238     note = "(paused)";
239   else if(playing_track) {
240     byte_asprintf(&p, "'%s' by %s, from '%s'",
241                   namepart(playing_track->track, "display", "title"),
242                   namepart(playing_track->track, "display", "artist"),
243                   namepart(playing_track->track, "display", "album"));
244     note = p;
245   } else if(!(last_state & DISORDER_PLAYING_ENABLED))
246     note = "(playing disabled)";
247   else if(!(last_state & DISORDER_RANDOM_ENABLED))
248     note = "(random play disabled)";
249   else
250     note = "(nothing to play for unknown reason)";
251
252   if(last_note && !strcmp(note, last_note))
253     return;
254   last_note = xstrdup(note);
255   byte_asprintf(&p, "Disobedience: %s", note);
256   gtk_window_set_title(GTK_WINDOW(toplevel), p);
257 }
258
259 /** @brief Create and populate the main window */
260 static void make_toplevel_window(void) {
261   GtkWidget *const vbox = gtk_vbox_new(FALSE/*homogeneous*/, 1/*spacing*/);
262   GtkWidget *const rb = report_box();
263
264   D(("top_window"));
265   toplevel = gtk_window_new(GTK_WINDOW_TOPLEVEL);
266   /* default size is too small */
267   gtk_window_set_default_size(GTK_WINDOW(toplevel),
268                               toplevel_width, toplevel_height);
269   /* terminate on close */
270   g_signal_connect(G_OBJECT(toplevel), "delete_event",
271                    G_CALLBACK(delete_event), NULL);
272   /* track size */
273   g_signal_connect(G_OBJECT(toplevel), "size-allocate",
274                    G_CALLBACK(toplevel_size_allocate), NULL);
275   /* lay out the window */
276   hack_window_title(0, 0, 0);
277   gtk_container_add(GTK_CONTAINER(toplevel), vbox);
278   /* lay out the vbox */
279   gtk_box_pack_start(GTK_BOX(vbox),
280                      menubar(toplevel),
281                      FALSE,             /* expand */
282                      FALSE,             /* fill */
283                      0);
284   gtk_box_pack_start(GTK_BOX(vbox),
285                      control_widget(),
286                      FALSE,             /* expand */
287                      FALSE,             /* fill */
288                      0);
289   playing_mini = playing_widget();
290   gtk_box_pack_start(GTK_BOX(vbox),
291                      playing_mini,
292                      FALSE,
293                      FALSE,
294                      0);
295   notebook_box = gtk_vbox_new(FALSE, 0);
296   gtk_container_add(GTK_CONTAINER(notebook_box), notebook());
297   gtk_container_add(GTK_CONTAINER(vbox), notebook_box);
298   gtk_box_pack_end(GTK_BOX(vbox),
299                    rb,
300                    FALSE,             /* expand */
301                    FALSE,             /* fill */
302                    0);
303   gtk_widget_set_style(toplevel, tool_style);
304   event_register("mini-mode-changed", main_minimode, 0);
305   event_register("periodic-fast", check_toplevel_size, 0);
306   event_register("playing-track-changed", hack_window_title, 0);
307   event_register("enabled-changed", hack_window_title, 0);
308   event_register("random-changed", hack_window_title, 0);
309   event_register("pause-changed", hack_window_title, 0);
310   event_register("playing-changed", hack_window_title, 0);
311   event_register("connected-changed", hack_window_title, 0);
312   event_register("lookups-completed", hack_window_title, 0);
313 }
314
315 static void userinfo_rights_completed(void attribute((unused)) *v,
316                                       const char *err,
317                                       const char *value) {
318   rights_type r;
319
320   if(err) {
321     popup_protocol_error(0, err);
322     r = 0;
323   } else {
324     if(parse_rights(value, &r, 0))
325       r = 0;
326   }
327   /* If rights have changed, signal everything that cares */
328   if(r != last_rights) {
329     last_rights = r;
330     ++suppress_actions;
331     event_raise("rights-changed", 0);
332     --suppress_actions;
333   }
334   rights_lookup_in_flight = 0;
335 }
336
337 static void check_rights(void) {
338   if(!rights_lookup_in_flight) {
339     rights_lookup_in_flight = 1;
340     disorder_eclient_userinfo(client,
341                               userinfo_rights_completed,
342                               config->username, "rights",
343                               0);
344   }
345 }
346
347 /** @brief Called occasionally */
348 static gboolean periodic_slow(gpointer attribute((unused)) data) {
349   D(("periodic_slow"));
350   /* Expire cached data */
351   cache_expire();
352   /* Update everything to be sure that the connection to the server hasn't
353    * mysteriously gone stale on us. */
354   all_update();
355   event_raise("periodic-slow", 0);
356   /* Recheck RTP status too */
357   check_rtp_address(0, 0, 0);
358   return TRUE;                          /* don't remove me */
359 }
360
361 /** @brief Called frequently */
362 static gboolean periodic_fast(gpointer attribute((unused)) data) {
363 #if 0                                   /* debugging hack */
364   static struct timeval last;
365   struct timeval now;
366   double delta;
367
368   xgettimeofday(&now, 0);
369   if(last.tv_sec) {
370     delta = (now.tv_sec + now.tv_sec / 1.0E6) 
371       - (last.tv_sec + last.tv_sec / 1.0E6);
372     if(delta >= 1.0625)
373       fprintf(stderr, "%f: %fs between 1s heartbeats\n", 
374               now.tv_sec + now.tv_sec / 1.0E6,
375               delta);
376   }
377   last = now;
378 #endif
379   if(rtp_supported) {
380     int nl, nr;
381     if (!rtp_getvol(&nl, &nr) && (nl != volume_l || nr != volume_r)) {
382       volume_l = nl;
383       volume_r = nr;
384       event_raise("volume-changed", 0);
385     }
386   }
387   /* Periodically check what our rights are */
388   int recheck_rights = 1;
389   if(server_version_bytes >= 0x04010000)
390     /* Server versions after 4.1 will send updates */
391     recheck_rights = 0;
392   if((server_version_bytes & 0xFF) == 0x01)
393     /* Development servers might do regardless of their version number */
394     recheck_rights = 0;
395   if(recheck_rights)
396     check_rights();
397   event_raise("periodic-fast", 0);
398   return TRUE;
399 }
400
401 /** @brief Called when a NOP completes */
402 static void nop_completed(void attribute((unused)) *v,
403                           const char attribute((unused)) *err) {
404   /* TODO report the error somewhere */
405   nop_in_flight = 0;
406 }
407
408 /** @brief Called from time to time to arrange for a NOP to be sent
409  *
410  * At most one NOP remains in flight at any moment.  If the client is not
411  * currently connected then no NOP is sent.
412  */
413 static gboolean maybe_send_nop(gpointer attribute((unused)) data) {
414   if(!nop_in_flight && (disorder_eclient_state(client) & DISORDER_CONNECTED)) {
415     nop_in_flight = 1;
416     disorder_eclient_nop(client, nop_completed, 0);
417   }
418   if(rtp_supported) {
419     const int rtp_was_running = rtp_is_running;
420     rtp_is_running = rtp_running();
421     if(rtp_was_running != rtp_is_running)
422       event_raise("rtp-changed", 0);
423   }
424   return TRUE;                          /* keep call me please */
425 }
426
427 /** @brief Called when a rtp-address command succeeds */
428 static void got_rtp_address(void attribute((unused)) *v,
429                             const char *err,
430                             int attribute((unused)) nvec,
431                             char attribute((unused)) **vec) {
432   const int rtp_was_supported = rtp_supported;
433   const int rtp_was_running = rtp_is_running;
434
435   ++suppress_actions;
436   rtp_address_in_flight = 0;
437   if(err) {
438     /* An error just means that we're not using network play */
439     rtp_supported = 0;
440     rtp_is_running = 0;
441   } else {
442     rtp_supported = 1;
443     rtp_is_running = rtp_running();
444   }
445   /*fprintf(stderr, "rtp supported->%d, running->%d\n",
446           rtp_supported, rtp_is_running);*/
447   if(rtp_supported != rtp_was_supported
448      || rtp_is_running != rtp_was_running)
449     event_raise("rtp-changed", 0);
450   --suppress_actions;
451 }
452
453 /** @brief Called to check whether RTP play is available */
454 static void check_rtp_address(const char attribute((unused)) *event,
455                               void attribute((unused)) *eventdata,
456                               void attribute((unused)) *callbackdata) {
457   if(!rtp_address_in_flight) {
458     //fprintf(stderr, "checking rtp\n");
459     disorder_eclient_rtp_address(client, got_rtp_address, NULL);
460   }
461 }
462
463 /* main -------------------------------------------------------------------- */
464
465 static const struct option options[] = {
466   { "help", no_argument, 0, 'h' },
467   { "version", no_argument, 0, 'V' },
468   { "config", required_argument, 0, 'c' },
469   { "tufnel", no_argument, 0, 't' },
470   { "debug", no_argument, 0, 'd' },
471   { 0, 0, 0, 0 }
472 };
473
474 /* display usage message and terminate */
475 static void attribute((noreturn)) help(void) {
476   xprintf("Disobedience - GUI client for DisOrder\n"
477           "\n"
478           "Usage:\n"
479           "  disobedience [OPTIONS]\n"
480           "Options:\n"
481           "  --help, -h              Display usage message\n"
482           "  --version, -V           Display version number\n"
483           "  --config PATH, -c PATH  Set configuration file\n"
484           "  --debug, -d             Turn on debugging\n"
485           "\n"
486           "Also GTK+ options will work.\n");
487   xfclose(stdout);
488   exit(0);
489 }
490
491 static void version_completed(void attribute((unused)) *v,
492                               const char attribute((unused)) *err,
493                               const char *ver) {
494   long major, minor, patch, dev;
495
496   if(!ver) {
497     server_version = 0;
498     server_version_bytes = 0;
499     return;
500   }
501   server_version = ver;
502   server_version_bytes = 0;
503   major = strtol(ver, (char **)&ver, 10);
504   if(*ver != '.')
505     return;
506   ++ver;
507   minor = strtol(ver, (char **)&ver, 10);
508   if(*ver == '.') {
509     ++ver;
510     patch = strtol(ver, (char **)&ver, 10);
511   } else
512     patch = 0;
513   if(*ver) {
514     if(*ver == '+') {
515       dev = 1;
516       ++ver;
517     }
518     if(*ver)
519       dev = 2;
520   } else
521     dev = 0;
522   server_version_bytes = (major << 24) + (minor << 16) + (patch << 8) + dev;
523 }
524
525 void logged_in(void) {
526   /* reset the clients */
527   disorder_eclient_close(client);
528   disorder_eclient_close(logclient);
529   rtp_supported = 0;
530   event_raise("logged-in", 0);
531   /* Force the periodic checks */
532   periodic_slow(0);
533   periodic_fast(0);
534   /* Recheck server version */
535   disorder_eclient_version(client, version_completed, 0);
536   disorder_eclient_enable_connect(client);
537   disorder_eclient_enable_connect(logclient);
538 }
539
540 int main(int argc, char **argv) {
541   int n;
542   gboolean gtkok;
543
544   mem_init();
545   /* garbage-collect PCRE's memory */
546   regexp_setup();
547   if(!setlocale(LC_CTYPE, "")) disorder_fatal(errno, "error calling setlocale");
548   gtkok = gtk_init_check(&argc, &argv);
549   while((n = getopt_long(argc, argv, "hVc:dtHC", options, 0)) >= 0) {
550     switch(n) {
551     case 'h': help();
552     case 'V': version("disobedience");
553     case 'c': configfile = optarg; break;
554     case 'd': debugging = 1; break;
555     case 't': goesupto = 11; break;
556     default: disorder_fatal(0, "invalid option");
557     }
558   }
559   if(!gtkok)
560     disorder_fatal(0, "failed to initialize GTK+");
561   /* gcrypt initialization */
562   if(!gcry_check_version(NULL))
563     disorder_fatal(0, "gcry_check_version failed");
564   gcry_control(GCRYCTL_INIT_SECMEM, 0);
565   gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0);
566   signal(SIGPIPE, SIG_IGN);
567   init_styles();
568   load_settings();
569   /* create the event loop */
570   D(("create main loop"));
571   mainloop = g_main_loop_new(0, 0);
572   if(config_read(0, NULL)) disorder_fatal(0, "cannot read configuration");
573   /* create the clients */
574   if(!(client = gtkclient())
575      || !(logclient = gtkclient()))
576     return 1;                           /* already reported an error */
577   /* periodic operations (e.g. expiring the cache, checking local volume) */
578   g_timeout_add(600000/*milliseconds*/, periodic_slow, 0);
579   g_timeout_add(1000/*milliseconds*/, periodic_fast, 0);
580   make_toplevel_window();
581   /* reset styles now everything has its name */
582   gtk_rc_reset_styles(gtk_settings_get_for_screen(gdk_screen_get_default()));
583   gtk_widget_show_all(toplevel);
584   gtk_widget_hide(playing_mini);
585   /* issue a NOP every so often */
586   g_timeout_add_full(G_PRIORITY_LOW,
587                      2000/*interval, ms*/,
588                      maybe_send_nop,
589                      0/*data*/,
590                      0/*notify*/);
591   /* Start monitoring the log */
592   disorder_eclient_log(logclient, &log_callbacks, 0);
593   /* Initiate all the checks */
594   periodic_fast(0);
595   disorder_eclient_version(client, version_completed, 0);
596   event_register("log-connected", check_rtp_address, 0);
597   suppress_actions = 0;
598   playlists_init();
599   globals_init();
600   /* If no password is set yet pop up a login box */
601   if(!config->password)
602     login_box();
603   D(("enter main loop"));
604   g_main_loop_run(mainloop);
605   return 0;
606 }
607
608 /*
609 Local Variables:
610 c-basic-offset:2
611 comment-column:40
612 fill-column:79
613 indent-tabs-mode:nil
614 End:
615 */