chiark / gitweb /
title and better prompts for overwrite window
[disorder] / disobedience / disobedience.c
1 /*
2  * This file is part of DisOrder.
3  * Copyright (C) 2006, 2007 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 2 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, but
11  * WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * 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, write to the Free Software
17  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
18  * USA
19  */
20 /** @file disobedience/disobedience.c
21  * @brief Main Disobedience program
22  */
23
24 #include "disobedience.h"
25
26 #include <getopt.h>
27 #include <locale.h>
28 #include <pcre.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 #include "style.h"                      /* generated style */
34
35 /* Variables --------------------------------------------------------------- */
36
37 /** @brief Event loop */
38 GMainLoop *mainloop;
39
40 /** @brief Top-level window */
41 GtkWidget *toplevel;
42
43 /** @brief Label for progress indicator */
44 GtkWidget *report_label;
45
46 /** @brief Main tab group */
47 GtkWidget *tabs;
48
49 /** @brief Main client */
50 disorder_eclient *client;
51
52 /** @brief Log client */
53 disorder_eclient *logclient;
54
55 /** @brief Last reported state
56  *
57  * This is updated by log_state().
58  */
59 unsigned long last_state;
60
61 /** @brief True if some track is playing
62  *
63  * This ought to be removed in favour of last_state & DISORDER_PLAYING
64  */
65 int playing;
66
67 /** @brief Left channel volume */
68 int volume_l;
69
70 /** @brief Right channel volume */
71 int volume_r;
72
73 double goesupto = 10;                   /* volume upper bound */
74
75 /** @brief True if a NOP is in flight */
76 static int nop_in_flight;
77
78 /** @brief Global tooltip group */
79 GtkTooltips *tips;
80
81 /** @brief Linked list of functions to call when we reset login parameters */
82 static struct reset_callback_node {
83   struct reset_callback_node *next;
84   reset_callback *callback;
85 } *resets;
86
87 /* Window creation --------------------------------------------------------- */
88
89 /* Note that all the client operations kicked off from here will only complete
90  * after we've entered the event loop. */
91
92 /** @brief Called when main window is deleted
93  *
94  * Terminates the program.
95  */
96 static gboolean delete_event(GtkWidget attribute((unused)) *widget,
97                              GdkEvent attribute((unused)) *event,
98                              gpointer attribute((unused)) data) {
99   D(("delete_event"));
100   exit(0);                              /* die immediately */
101 }
102
103 /** @brief Called when the current tab is switched
104  *
105  * Updates the menu settings to correspond to the new page.
106  */
107 static void tab_switched(GtkNotebook attribute((unused)) *notebook,
108                          GtkNotebookPage attribute((unused)) *page,
109                          guint page_num,
110                          gpointer attribute((unused)) user_data) {
111   menu_update(page_num);
112 }
113
114 /** @brief Create the report box */
115 static GtkWidget *report_box(void) {
116   GtkWidget *vbox = gtk_vbox_new(FALSE, 0);
117
118   report_label = gtk_label_new("");
119   gtk_label_set_ellipsize(GTK_LABEL(report_label), PANGO_ELLIPSIZE_END);
120   gtk_misc_set_alignment(GTK_MISC(report_label), 0, 0);
121   gtk_container_add(GTK_CONTAINER(vbox), gtk_hseparator_new());
122   gtk_container_add(GTK_CONTAINER(vbox), report_label);
123   return vbox;
124 }
125
126 /** @brief Create and populate the main tab group */
127 static GtkWidget *notebook(void) {
128   tabs = gtk_notebook_new();
129   g_signal_connect(tabs, "switch-page", G_CALLBACK(tab_switched), 0);
130   gtk_notebook_append_page(GTK_NOTEBOOK(tabs), queue_widget(),
131                            gtk_label_new("Queue"));
132   gtk_notebook_append_page(GTK_NOTEBOOK(tabs), recent_widget(),
133                            gtk_label_new("Recent"));
134   gtk_notebook_append_page(GTK_NOTEBOOK(tabs), choose_widget(),
135                            gtk_label_new("Choose"));
136   gtk_notebook_append_page(GTK_NOTEBOOK(tabs), added_widget(),
137                            gtk_label_new("Added"));
138   return tabs;
139 }
140
141 /** @brief Create and populate the main window */
142 static void make_toplevel_window(void) {
143   GtkWidget *const vbox = gtk_vbox_new(FALSE, 1);
144   GtkWidget *const rb = report_box();
145
146   D(("top_window"));
147   toplevel = gtk_window_new(GTK_WINDOW_TOPLEVEL);
148   /* default size is too small */
149   gtk_window_set_default_size(GTK_WINDOW(toplevel), 640, 480);
150   /* terminate on close */
151   g_signal_connect(G_OBJECT(toplevel), "delete_event",
152                    G_CALLBACK(delete_event), NULL);
153   /* lay out the window */
154   gtk_window_set_title(GTK_WINDOW(toplevel), "Disobedience");
155   gtk_container_add(GTK_CONTAINER(toplevel), vbox);
156   /* lay out the vbox */
157   gtk_box_pack_start(GTK_BOX(vbox),
158                      menubar(toplevel),
159                      FALSE,             /* expand */
160                      FALSE,             /* fill */
161                      0);
162   gtk_box_pack_start(GTK_BOX(vbox),
163                      control_widget(),
164                      FALSE,             /* expand */
165                      FALSE,             /* fill */
166                      0);
167   gtk_container_add(GTK_CONTAINER(vbox), notebook());
168   gtk_box_pack_end(GTK_BOX(vbox),
169                    rb,
170                    FALSE,             /* expand */
171                    FALSE,             /* fill */
172                    0);
173   gtk_widget_set_name(toplevel, "disobedience");
174 }
175
176 #if MDEBUG
177 static int widget_count, container_count;
178
179 static void count_callback(GtkWidget *w,
180                            gpointer attribute((unused)) data) {
181   ++widget_count;
182   if(GTK_IS_CONTAINER(w)) {
183     ++container_count;
184     gtk_container_foreach(GTK_CONTAINER(w), count_callback, 0);
185   }
186 }
187
188 static void count_widgets(void) {
189   widget_count = 0;
190   container_count = 1;
191   if(toplevel)
192     gtk_container_foreach(GTK_CONTAINER(toplevel), count_callback, 0);
193   fprintf(stderr, "widget count: %8d  container count: %8d\n",
194           widget_count, container_count);
195 }
196 #endif
197
198 #if MTRACK
199 const char *mtag = "init";
200 static hash *mtrack_hash;
201
202 static int *mthfind(const char *tag) {
203   static const int zero = 0;
204   int *cp = hash_find(mtrack_hash, tag);
205   if(!cp) {
206     hash_add(mtrack_hash, tag, &zero, HASH_INSERT);
207     cp = hash_find(mtrack_hash, tag);
208   }
209   return cp;
210 }
211
212 static void *trap_malloc(size_t n) {
213   void *ptr = malloc(n + sizeof(char *));
214
215   *(const char **)ptr = mtag;
216   ++*mthfind(mtag);
217   return (char *)ptr + sizeof(char *);
218 }
219
220 static void trap_free(void *ptr) {
221   const char *tag;
222   if(!ptr)
223     return;
224   ptr = (char *)ptr - sizeof(char *);
225   tag = *(const char **)ptr;
226   --*mthfind(tag);
227   free(ptr);
228 }
229
230 static void *trap_realloc(void *ptr, size_t n) {
231   if(!ptr)
232     return trap_malloc(n);
233   if(!n) {
234     trap_free(ptr);
235     return 0;
236   }
237   ptr = (char *)ptr - sizeof(char *);
238   ptr = realloc(ptr, n + sizeof(char *));
239   *(const char **)ptr = mtag;
240   return (char *)ptr + sizeof(char *);
241 }
242
243 static int report_tags_callback(const char *key, void *value,
244                                 void attribute((unused)) *u) {
245   fprintf(stderr, "%16s: %d\n", key, *(int *)value);
246   return 0;
247 }
248
249 static void report_tags(void) {
250   hash_foreach(mtrack_hash, report_tags_callback, 0);
251   fprintf(stderr, "\n");
252 }
253
254 static const GMemVTable glib_memvtable = {
255   trap_malloc,
256   trap_realloc,
257   trap_free,
258   0,
259   0,
260   0
261 };
262 #endif
263
264 /** @brief Called once every 10 minutes */
265 static gboolean periodic(gpointer attribute((unused)) data) {
266   D(("periodic"));
267   /* Expire cached data */
268   cache_expire();
269   /* Update everything to be sure that the connection to the server hasn't
270    * mysteriously gone stale on us. */
271   all_update();
272 #if MDEBUG
273   count_widgets();
274   fprintf(stderr, "cache size: %zu\n", cache_count());
275 #endif
276 #if MTRACK
277   report_tags();
278 #endif
279   return TRUE;                          /* don't remove me */
280 }
281
282 /** @brief Called from time to time
283  *
284  * Used for debugging purposes
285  */
286 static gboolean heartbeat(gpointer attribute((unused)) data) {
287   static struct timeval last;
288   struct timeval now;
289   double delta;
290
291   xgettimeofday(&now, 0);
292   if(last.tv_sec) {
293     delta = (now.tv_sec + now.tv_sec / 1.0E6) 
294       - (last.tv_sec + last.tv_sec / 1.0E6);
295     if(delta >= 1.0625)
296       fprintf(stderr, "%f: %fs between 1s heartbeats\n", 
297               now.tv_sec + now.tv_sec / 1.0E6,
298               delta);
299   }
300   last = now;
301   return TRUE;
302 }
303
304 /** @brief Called when a NOP completes */
305 static void nop_completed(void attribute((unused)) *v) {
306   nop_in_flight = 0;
307 }
308
309 /** @brief Called from time to time to arrange for a NOP to be sent
310  *
311  * At most one NOP remains in flight at any moment.  If the client is not
312  * currently connected then no NOP is sent.
313  */
314 static gboolean maybe_send_nop(gpointer attribute((unused)) data) {
315   if(!nop_in_flight && (disorder_eclient_state(client) & DISORDER_CONNECTED)) {
316     nop_in_flight = 1;
317     disorder_eclient_nop(client, nop_completed, 0);
318   }
319   return TRUE;                          /* keep call me please */
320 }
321
322 /* main -------------------------------------------------------------------- */
323
324 static const struct option options[] = {
325   { "help", no_argument, 0, 'h' },
326   { "version", no_argument, 0, 'V' },
327   { "config", required_argument, 0, 'c' },
328   { "tufnel", no_argument, 0, 't' },
329   { "debug", no_argument, 0, 'd' },
330   { 0, 0, 0, 0 }
331 };
332
333 /* display usage message and terminate */
334 static void help(void) {
335   xprintf("Disobedience - GUI client for DisOrder\n"
336           "\n"
337           "Usage:\n"
338           "  disobedience [OPTIONS]\n"
339           "Options:\n"
340           "  --help, -h              Display usage message\n"
341           "  --version, -V           Display version number\n"
342           "  --config PATH, -c PATH  Set configuration file\n"
343           "  --debug, -d             Turn on debugging\n"
344           "\n"
345           "Also GTK+ options will work.\n");
346   xfclose(stdout);
347   exit(0);
348 }
349
350 /* display version number and terminate */
351 static void version(void) {
352   xprintf("disorder version %s\n", disorder_version_string);
353   xfclose(stdout);
354   exit(0);
355 }
356
357 /* reset state */
358 void reset(void) {
359   struct reset_callback_node *r;
360
361   /* reset the clients */
362   disorder_eclient_close(client);
363   disorder_eclient_close(logclient);
364   for(r = resets; r; r = r->next)
365     r->callback();
366 }
367
368 /** @brief Register a reset callback */
369 void register_reset(reset_callback *callback) {
370   struct reset_callback_node *const r = xmalloc(sizeof *r);
371
372   r->next = resets;
373   r->callback = callback;
374   resets = r;
375 }
376
377 int main(int argc, char **argv) {
378   int n;
379
380   mem_init();
381   /* garbage-collect PCRE's memory */
382   pcre_malloc = xmalloc;
383   pcre_free = xfree;
384 #if MTRACK
385   mtrack_hash = hash_new(sizeof (int));
386   g_mem_set_vtable((GMemVTable *)&glib_memvtable);
387 #endif
388   if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
389   gtk_init(&argc, &argv);
390   gtk_rc_parse_string(style);
391   while((n = getopt_long(argc, argv, "hVc:dtHC", options, 0)) >= 0) {
392     switch(n) {
393     case 'h': help();
394     case 'V': version();
395     case 'c': configfile = optarg; break;
396     case 'd': debugging = 1; break;
397     case 't': goesupto = 11; break;
398     default: fatal(0, "invalid option");
399     }
400   }
401   signal(SIGPIPE, SIG_IGN);
402   /* create the event loop */
403   D(("create main loop"));
404   mainloop = g_main_loop_new(0, 0);
405   if(config_read(0)) fatal(0, "cannot read configuration");
406   /* create the clients */
407   if(!(client = gtkclient())
408      || !(logclient = gtkclient()))
409     return 1;                           /* already reported an error */
410   /* periodic operations (e.g. expiring the cache) */
411 #if MDEBUG || MTRACK
412   g_timeout_add(5000/*milliseconds*/, periodic, 0);
413 #else
414   g_timeout_add(600000/*milliseconds*/, periodic, 0);
415 #endif
416   /* The point of this is to try and get a handle on mysterious
417    * unresponsiveness.  It's not very useful in production use. */
418   if(0)
419     g_timeout_add(1000/*milliseconds*/, heartbeat, 0);
420   /* global tooltips */
421   tips = gtk_tooltips_new();
422   make_toplevel_window();
423   /* reset styles now everything has its name */
424   gtk_rc_reset_styles(gtk_settings_get_for_screen(gdk_screen_get_default()));
425   gtk_widget_show_all(toplevel);
426   /* issue a NOP every so often */
427   g_timeout_add_full(G_PRIORITY_LOW,
428                      1000/*interval, ms*/,
429                      maybe_send_nop,
430                      0/*data*/,
431                      0/*notify*/);
432   register_reset(properties_reset);
433   /* Start monitoring the log */
434   disorder_eclient_log(logclient, &log_callbacks, 0);
435   D(("enter main loop"));
436   MTAG("misc");
437   g_main_loop_run(mainloop);
438   return 0;
439 }
440
441 /*
442 Local Variables:
443 c-basic-offset:2
444 comment-column:40
445 fill-column:79
446 indent-tabs-mode:nil
447 End:
448 */