]> git.armaanb.net Git - chorizo.git/blob - browser.c
Merge pull request #39 from onodera-punpun/load
[chorizo.git] / browser.c
1 #include <limits.h>
2 #include <stdio.h>
3 #include <stdlib.h>
4 #include <sys/types.h>
5 #include <sys/stat.h>
6 #include <fcntl.h>
7 #include <string.h>
8
9 #include <gtk/gtk.h>
10 #include <gtk/gtkx.h>
11 #include <gdk/gdkkeysyms.h>
12 #include <gio/gio.h>
13 #include <webkit2/webkit2.h>
14
15
16 static void client_destroy(GtkWidget *, gpointer);
17 static gboolean client_destroy_request(WebKitWebView *, gpointer);
18 static WebKitWebView *client_new(const gchar *, WebKitWebView *, gboolean);
19 static WebKitWebView *client_new_request(WebKitWebView *, WebKitNavigationAction *,
20                                          gpointer);
21 static void cooperation_setup(void);
22 static void changed_download_progress(GObject *, GParamSpec *, gpointer);
23 static void changed_load_progress(GObject *, GParamSpec *, gpointer);
24 static void changed_title(GObject *, GParamSpec *, gpointer);
25 static void changed_uri(GObject *, GParamSpec *, gpointer);
26 static gboolean crashed_web_view(WebKitWebView *, gpointer);
27 static gboolean decide_policy(WebKitWebView *, WebKitPolicyDecision *,
28                               WebKitPolicyDecisionType, gpointer);
29 static gboolean download_handle(WebKitDownload *, gchar *, gpointer);
30 static void download_handle_start(WebKitWebView *, WebKitDownload *, gpointer);
31 static void downloadmanager_cancel(GtkToolButton *, gpointer data);
32 static gboolean downloadmanager_delete(GtkWidget *, gpointer);
33 static void downloadmanager_setup(void);
34 static gchar *ensure_uri_scheme(const gchar *);
35 static void external_handler_run(GtkAction *, gpointer);
36 static void grab_environment_configuration(void);
37 static void hover_web_view(WebKitWebView *, WebKitHitTestResult *, guint, gpointer);
38 static gboolean key_common(GtkWidget *, GdkEvent *, gpointer);
39 static gboolean key_downloadmanager(GtkWidget *, GdkEvent *, gpointer);
40 static gboolean key_location(GtkWidget *, GdkEvent *, gpointer);
41 static gboolean key_web_view(GtkWidget *, GdkEvent *, gpointer);
42 static void keywords_load(void);
43 static gboolean keywords_try_search(WebKitWebView *, const gchar *);
44 static gboolean menu_web_view(WebKitWebView *, WebKitContextMenu *, GdkEvent *,
45                               WebKitHitTestResult *, gpointer);
46 static gboolean quit_if_nothing_active(void);
47 static gboolean remote_msg(GIOChannel *, GIOCondition, gpointer);
48 static void search(gpointer, gint);
49 static void show_web_view(WebKitWebView *, gpointer);
50 static Window tabbed_launch(void);
51 static void trust_user_certs(WebKitWebContext *);
52
53
54 struct Client
55 {
56     gchar *external_handler_uri;
57     gchar *hover_uri;
58     GtkWidget *location;
59     GtkWidget *vbox;
60     GtkWidget *web_view;
61     GtkWidget *win;
62 };
63
64 struct DownloadManager
65 {
66     GtkWidget *scroll;
67     GtkWidget *toolbar;
68     GtkWidget *win;
69 } dm;
70
71
72 static const gchar *accepted_language[2] = { NULL, NULL };
73 static gint clients = 0, downloads = 0;
74 static gboolean cooperative_alone = TRUE;
75 static gboolean cooperative_instances = TRUE;
76 static int cooperative_pipe_fp = 0;
77 static gchar *download_dir = "/var/tmp";
78 static gboolean enable_webgl = FALSE;
79 static Window embed = 0;
80 static gchar *fifo_suffix = "main";
81 static gdouble global_zoom = 1.0;
82 static gchar *history_file = NULL;
83 static gchar *home_uri = "about:blank";
84 static gboolean initial_wc_setup_done = FALSE;
85 static GHashTable *keywords = NULL;
86 static gchar *search_text = NULL;
87 static gboolean tabbed_automagic = TRUE;
88 static gchar *user_agent = NULL;
89
90
91 void
92 client_destroy(GtkWidget *widget, gpointer data)
93 {
94     struct Client *c = (struct Client *)data;
95
96     g_signal_handlers_disconnect_by_func(G_OBJECT(c->web_view),
97                                          changed_load_progress, c);
98
99     free(c);
100     clients--;
101
102     quit_if_nothing_active();
103 }
104
105 gboolean
106 client_destroy_request(WebKitWebView *web_view, gpointer data)
107 {
108     struct Client *c = (struct Client *)data;
109
110     gtk_widget_destroy(c->win);
111
112     return TRUE;
113 }
114
115 WebKitWebView *
116 client_new(const gchar *uri, WebKitWebView *related_wv, gboolean show)
117 {
118     struct Client *c;
119     WebKitWebContext *wc;
120     gchar *f;
121
122     if (uri != NULL && cooperative_instances && !cooperative_alone)
123     {
124         f = ensure_uri_scheme(uri);
125         write(cooperative_pipe_fp, f, strlen(f));
126         write(cooperative_pipe_fp, "\n", 1);
127         g_free(f);
128         return NULL;
129     }
130
131     c = calloc(1, sizeof(struct Client));
132     if (!c)
133     {
134         fprintf(stderr, __NAME__": fatal: calloc failed\n");
135         exit(EXIT_FAILURE);
136     }
137
138     if (embed != 0)
139     {
140         c->win = gtk_plug_new(embed);
141         if (!gtk_plug_get_embedded(GTK_PLUG(c->win)))
142         {
143             fprintf(stderr, __NAME__": Can't plug-in to XID %ld.\n", embed);
144             gtk_widget_destroy(c->win);
145             c->win = NULL;
146             embed = 0;
147         }
148     }
149
150     if (c->win == NULL)
151         c->win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
152
153     gtk_window_set_default_size(GTK_WINDOW(c->win), 800, 600);
154
155     g_signal_connect(G_OBJECT(c->win), "destroy", G_CALLBACK(client_destroy), c);
156     gtk_window_set_title(GTK_WINDOW(c->win), __NAME__);
157
158     if (related_wv == NULL)
159         c->web_view = webkit_web_view_new();
160     else
161         c->web_view = webkit_web_view_new_with_related_view(related_wv);
162     wc = webkit_web_view_get_context(WEBKIT_WEB_VIEW(c->web_view));
163
164     webkit_web_view_set_zoom_level(WEBKIT_WEB_VIEW(c->web_view), global_zoom);
165     g_signal_connect(G_OBJECT(c->web_view), "notify::title",
166                      G_CALLBACK(changed_title), c);
167     g_signal_connect(G_OBJECT(c->web_view), "notify::uri",
168                      G_CALLBACK(changed_uri), c);
169     g_signal_connect(G_OBJECT(c->web_view), "notify::estimated-load-progress",
170                      G_CALLBACK(changed_load_progress), c);
171     g_signal_connect(G_OBJECT(c->web_view), "create",
172                      G_CALLBACK(client_new_request), NULL);
173     g_signal_connect(G_OBJECT(c->web_view), "context-menu",
174                      G_CALLBACK(menu_web_view), c);
175     g_signal_connect(G_OBJECT(c->web_view), "close",
176                      G_CALLBACK(client_destroy_request), c);
177     g_signal_connect(G_OBJECT(c->web_view), "decide-policy",
178                      G_CALLBACK(decide_policy), NULL);
179     g_signal_connect(G_OBJECT(c->web_view), "key-press-event",
180                      G_CALLBACK(key_web_view), c);
181     g_signal_connect(G_OBJECT(c->web_view), "button-press-event",
182                      G_CALLBACK(key_web_view), c);
183     g_signal_connect(G_OBJECT(c->web_view), "scroll-event",
184                      G_CALLBACK(key_web_view), c);
185     g_signal_connect(G_OBJECT(c->web_view), "mouse-target-changed",
186                      G_CALLBACK(hover_web_view), c);
187     g_signal_connect(G_OBJECT(c->web_view), "web-process-crashed",
188                      G_CALLBACK(crashed_web_view), c);
189
190     if (!initial_wc_setup_done)
191     {
192         if (accepted_language[0] != NULL)
193             webkit_web_context_set_preferred_languages(wc, accepted_language);
194
195         g_signal_connect(G_OBJECT(wc), "download-started",
196                          G_CALLBACK(download_handle_start), NULL);
197
198         trust_user_certs(wc);
199
200         initial_wc_setup_done = TRUE;
201     }
202
203     if (user_agent != NULL)
204         g_object_set(G_OBJECT(webkit_web_view_get_settings(WEBKIT_WEB_VIEW(c->web_view))),
205                      "user-agent", user_agent, NULL);
206
207     if (enable_webgl)
208         webkit_settings_set_enable_webgl(webkit_web_view_get_settings(WEBKIT_WEB_VIEW(c->web_view)), TRUE);
209
210     c->location = gtk_entry_new();
211     g_signal_connect(G_OBJECT(c->location), "key-press-event",
212                      G_CALLBACK(key_location), c);
213
214     c->vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
215     gtk_box_pack_start(GTK_BOX(c->vbox), c->location, FALSE, FALSE, 0);
216     gtk_box_pack_start(GTK_BOX(c->vbox), c->web_view, TRUE, TRUE, 0);
217
218     gtk_container_add(GTK_CONTAINER(c->win), c->vbox);
219
220     if (show)
221         show_web_view(NULL, c);
222     else
223         g_signal_connect(G_OBJECT(c->web_view), "ready-to-show",
224                          G_CALLBACK(show_web_view), c);
225
226     if (uri != NULL)
227     {
228         f = ensure_uri_scheme(uri);
229         webkit_web_view_load_uri(WEBKIT_WEB_VIEW(c->web_view), f);
230         g_free(f);
231     }
232
233     clients++;
234
235     return WEBKIT_WEB_VIEW(c->web_view);
236 }
237
238 WebKitWebView *
239 client_new_request(WebKitWebView *web_view,
240                    WebKitNavigationAction *navigation_action, gpointer data)
241 {
242     return client_new(NULL, web_view, FALSE);
243 }
244
245 void
246 cooperation_setup(void)
247 {
248     GIOChannel *towatch;
249     gchar *fifofilename, *fifopath;
250
251     fifofilename = g_strdup_printf("%s-%s", __NAME__".fifo", fifo_suffix);
252     fifopath = g_build_filename(g_get_user_runtime_dir(), fifofilename, NULL);
253     g_free(fifofilename);
254
255     if (!g_file_test(fifopath, G_FILE_TEST_EXISTS))
256         mkfifo(fifopath, 0600);
257
258     cooperative_pipe_fp = open(fifopath, O_WRONLY | O_NONBLOCK);
259     if (!cooperative_pipe_fp)
260     {
261         fprintf(stderr, __NAME__": Can't open FIFO at all.\n");
262     }
263     else
264     {
265         if (write(cooperative_pipe_fp, "", 0) == -1)
266         {
267             /* Could not do an empty write to the FIFO which means there's
268              * no one listening. */
269             close(cooperative_pipe_fp);
270             towatch = g_io_channel_new_file(fifopath, "r+", NULL);
271             g_io_add_watch(towatch, G_IO_IN, (GIOFunc)remote_msg, NULL);
272         }
273         else
274             cooperative_alone = FALSE;
275     }
276
277     g_free(fifopath);
278 }
279
280 void
281 changed_download_progress(GObject *obj, GParamSpec *pspec, gpointer data)
282 {
283     WebKitDownload *download = WEBKIT_DOWNLOAD(obj);
284     WebKitURIResponse *resp;
285     GtkToolItem *tb = GTK_TOOL_ITEM(data);
286     gdouble p, size_mb;
287     const gchar *uri;
288     gchar *t, *filename, *base;
289
290     p = webkit_download_get_estimated_progress(download);
291     p = p > 1 ? 1 : p;
292     p = p < 0 ? 0 : p;
293     p *= 100;
294     resp = webkit_download_get_response(download);
295     size_mb = webkit_uri_response_get_content_length(resp) / 1e6;
296
297     uri = webkit_download_get_destination(download);
298     filename = g_filename_from_uri(uri, NULL, NULL);
299     if (filename == NULL)
300     {
301         /* This really should not happen because WebKit uses that URI to
302          * write to a file... */
303         fprintf(stderr, __NAME__": Could not construct file name from URI!\n");
304         t = g_strdup_printf("%s (%.0f%% of %.1f MB)",
305                             webkit_uri_response_get_uri(resp), p, size_mb);
306     }
307     else
308     {
309         base = g_path_get_basename(filename);
310         t = g_strdup_printf("%s (%.0f%% of %.1f MB)", base, p, size_mb);
311         g_free(filename);
312         g_free(base);
313     }
314     gtk_tool_button_set_label(GTK_TOOL_BUTTON(tb), t);
315     g_free(t);
316 }
317
318 void
319 changed_load_progress(GObject *obj, GParamSpec *pspec, gpointer data)
320 {
321     struct Client *c = (struct Client *)data;
322     gdouble p;
323
324     p = webkit_web_view_get_estimated_load_progress(WEBKIT_WEB_VIEW(c->web_view));
325     if (p == 1) p = 0;
326     gtk_entry_set_progress_fraction(GTK_ENTRY(c->location), p);
327 }
328
329 void
330 changed_title(GObject *obj, GParamSpec *pspec, gpointer data)
331 {
332     const gchar *t, *u;
333     struct Client *c = (struct Client *)data;
334
335     u = webkit_web_view_get_uri(WEBKIT_WEB_VIEW(c->web_view));
336     t = webkit_web_view_get_title(WEBKIT_WEB_VIEW(c->web_view));
337
338     u = u == NULL ? __NAME__ : u;
339     u = u[0] == 0 ? __NAME__ : u;
340
341     t = t == NULL ? u : t;
342     t = t[0] == 0 ? u : t;
343
344     gtk_window_set_title(GTK_WINDOW(c->win), t);
345 }
346
347 void
348 changed_uri(GObject *obj, GParamSpec *pspec, gpointer data)
349 {
350     const gchar *t;
351     struct Client *c = (struct Client *)data;
352     FILE *fp;
353
354     t = webkit_web_view_get_uri(WEBKIT_WEB_VIEW(c->web_view));
355
356     /* When a web process crashes, we get a "notify::uri" signal, but we
357      * can no longer read a meaningful URI. It's just an empty string
358      * now. Not updating the location bar in this scenario is important,
359      * because we would override the "WEB PROCESS CRASHED" message. */
360     if (t != NULL && strlen(t) > 0)
361     {
362         gtk_entry_set_text(GTK_ENTRY(c->location), t);
363
364         if (history_file != NULL)
365         {
366             fp = fopen(history_file, "a");
367             if (fp != NULL)
368             {
369                 fprintf(fp, "%s\n", t);
370                 fclose(fp);
371             }
372             else
373                 perror(__NAME__": Error opening history file");
374         }
375     }
376 }
377
378 gboolean
379 crashed_web_view(WebKitWebView *web_view, gpointer data)
380 {
381     gchar *t;
382     struct Client *c = (struct Client *)data;
383
384     t = g_strdup_printf("WEB PROCESS CRASHED: %s",
385                         webkit_web_view_get_uri(WEBKIT_WEB_VIEW(web_view)));
386     gtk_entry_set_text(GTK_ENTRY(c->location), t);
387     g_free(t);
388
389     return TRUE;
390 }
391
392 gboolean
393 decide_policy(WebKitWebView *web_view, WebKitPolicyDecision *decision,
394               WebKitPolicyDecisionType type, gpointer data)
395 {
396     WebKitResponsePolicyDecision *r;
397
398     switch (type)
399     {
400         case WEBKIT_POLICY_DECISION_TYPE_RESPONSE:
401             r = WEBKIT_RESPONSE_POLICY_DECISION(decision);
402             if (!webkit_response_policy_decision_is_mime_type_supported(r))
403                 webkit_policy_decision_download(decision);
404             else
405                 webkit_policy_decision_use(decision);
406             break;
407         default:
408             /* Use whatever default there is. */
409             return FALSE;
410     }
411     return TRUE;
412 }
413
414 void
415 download_handle_finished(WebKitDownload *download, gpointer data)
416 {
417     downloads--;
418 }
419
420 void
421 download_handle_start(WebKitWebView *web_view, WebKitDownload *download,
422                       gpointer data)
423 {
424     g_signal_connect(G_OBJECT(download), "decide-destination",
425                      G_CALLBACK(download_handle), data);
426 }
427
428 gboolean
429 download_handle(WebKitDownload *download, gchar *suggested_filename, gpointer data)
430 {
431     gchar *sug_clean, *path, *path2 = NULL, *uri;
432     GtkToolItem *tb;
433     int suffix = 1;
434     size_t i;
435
436     sug_clean = g_strdup(suggested_filename);
437     for (i = 0; i < strlen(sug_clean); i++)
438         if (sug_clean[i] == G_DIR_SEPARATOR)
439             sug_clean[i] = '_';
440
441     path = g_build_filename(download_dir, sug_clean, NULL);
442     path2 = g_strdup(path);
443     while (g_file_test(path2, G_FILE_TEST_EXISTS) && suffix < 1000)
444     {
445         g_free(path2);
446
447         path2 = g_strdup_printf("%s.%d", path, suffix);
448         suffix++;
449     }
450
451     if (suffix == 1000)
452     {
453         fprintf(stderr, __NAME__": Suffix reached limit for download.\n");
454         webkit_download_cancel(download);
455     }
456     else
457     {
458         uri = g_filename_to_uri(path2, NULL, NULL);
459         webkit_download_set_destination(download, uri);
460         g_free(uri);
461
462         tb = gtk_tool_button_new(NULL, NULL);
463         gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(tb), "gtk-delete");
464         gtk_tool_button_set_label(GTK_TOOL_BUTTON(tb), sug_clean);
465         gtk_toolbar_insert(GTK_TOOLBAR(dm.toolbar), tb, 0);
466         gtk_widget_show_all(dm.win);
467
468         g_signal_connect(G_OBJECT(download), "notify::estimated-progress",
469                          G_CALLBACK(changed_download_progress), tb);
470
471         downloads++;
472         g_signal_connect(G_OBJECT(download), "finished",
473                          G_CALLBACK(download_handle_finished), NULL);
474
475         g_object_ref(download);
476         g_signal_connect(G_OBJECT(tb), "clicked",
477                          G_CALLBACK(downloadmanager_cancel), download);
478     }
479
480     g_free(sug_clean);
481     g_free(path);
482     g_free(path2);
483
484     /* Propagate -- to whom it may concern. */
485     return FALSE;
486 }
487
488 void
489 downloadmanager_cancel(GtkToolButton *tb, gpointer data)
490 {
491     WebKitDownload *download = WEBKIT_DOWNLOAD(data);
492
493     webkit_download_cancel(download);
494     g_object_unref(download);
495
496     gtk_widget_destroy(GTK_WIDGET(tb));
497 }
498
499 gboolean
500 downloadmanager_delete(GtkWidget *obj, gpointer data)
501 {
502     if (!quit_if_nothing_active())
503         gtk_widget_hide(dm.win);
504
505     return TRUE;
506 }
507
508 void
509 downloadmanager_setup(void)
510 {
511     dm.win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
512     gtk_window_set_type_hint(GTK_WINDOW(dm.win), GDK_WINDOW_TYPE_HINT_DIALOG);
513     gtk_window_set_default_size(GTK_WINDOW(dm.win), 500, 250);
514     gtk_window_set_title(GTK_WINDOW(dm.win), __NAME__" - Download Manager");
515     g_signal_connect(G_OBJECT(dm.win), "delete-event",
516                      G_CALLBACK(downloadmanager_delete), NULL);
517     g_signal_connect(G_OBJECT(dm.win), "key-press-event",
518                      G_CALLBACK(key_downloadmanager), NULL);
519
520     dm.toolbar = gtk_toolbar_new();
521     gtk_orientable_set_orientation(GTK_ORIENTABLE(dm.toolbar),
522                                    GTK_ORIENTATION_VERTICAL);
523     gtk_toolbar_set_style(GTK_TOOLBAR(dm.toolbar), GTK_TOOLBAR_BOTH_HORIZ);
524     gtk_toolbar_set_show_arrow(GTK_TOOLBAR(dm.toolbar), FALSE);
525
526     dm.scroll = gtk_scrolled_window_new(NULL, NULL);
527     gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(dm.scroll),
528                                    GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
529     gtk_container_add(GTK_CONTAINER(dm.scroll), dm.toolbar);
530
531     gtk_container_add(GTK_CONTAINER(dm.win), dm.scroll);
532 }
533
534 gchar *
535 ensure_uri_scheme(const gchar *t)
536 {
537     gchar *f, *fabs;
538
539     f = g_ascii_strdown(t, -1);
540     if (!g_str_has_prefix(f, "http:") &&
541         !g_str_has_prefix(f, "https:") &&
542         !g_str_has_prefix(f, "file:") &&
543         !g_str_has_prefix(f, "about:"))
544     {
545         g_free(f);
546         fabs = realpath(t, NULL);
547         if (fabs != NULL)
548         {
549             f = g_strdup_printf("file://%s", fabs);
550             free(fabs);
551         }
552         else
553             f = g_strdup_printf("http://%s", t);
554         return f;
555     }
556     else
557         return g_strdup(t);
558 }
559
560 void
561 external_handler_run(GtkAction *action, gpointer data)
562 {
563     struct Client *c = (struct Client *)data;
564     gchar *argv[] = { "lariza-external-handler", "-u", NULL, NULL };
565     GPid pid;
566     GError *err = NULL;
567
568     (void)action;
569
570     argv[2] = c->external_handler_uri;
571     if (!g_spawn_async(NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL,
572                        &pid, &err))
573     {
574         fprintf(stderr, __NAME__": Could not launch key handler: %s\n",
575                 err->message);
576         g_error_free(err);
577     }
578     else
579         g_spawn_close_pid(pid);
580 }
581
582 void
583 grab_environment_configuration(void)
584 {
585     const gchar *e;
586
587     e = g_getenv(__NAME_UPPERCASE__"_ACCEPTED_LANGUAGE");
588     if (e != NULL)
589         accepted_language[0] = g_strdup(e);
590
591     e = g_getenv(__NAME_UPPERCASE__"_DOWNLOAD_DIR");
592     if (e != NULL)
593         download_dir = g_strdup(e);
594
595     e = g_getenv(__NAME_UPPERCASE__"_ENABLE_EXPERIMENTAL_WEBGL");
596     if (e != NULL)
597         enable_webgl = TRUE;
598
599     e = g_getenv(__NAME_UPPERCASE__"_FIFO_SUFFIX");
600     if (e != NULL)
601         fifo_suffix = g_strdup(e);
602
603     e = g_getenv(__NAME_UPPERCASE__"_HISTORY_FILE");
604     if (e != NULL)
605         history_file = g_strdup(e);
606
607     e = g_getenv(__NAME_UPPERCASE__"_HOME_URI");
608     if (e != NULL)
609         home_uri = g_strdup(e);
610
611     e = g_getenv(__NAME_UPPERCASE__"_USER_AGENT");
612     if (e != NULL)
613         user_agent = g_strdup(e);
614
615     e = g_getenv(__NAME_UPPERCASE__"_ZOOM");
616     if (e != NULL)
617         global_zoom = atof(e);
618 }
619
620 void
621 hover_web_view(WebKitWebView *web_view, WebKitHitTestResult *ht, guint modifiers,
622                gpointer data)
623 {
624     struct Client *c = (struct Client *)data;
625
626     if (!gtk_widget_is_focus(c->location))
627     {
628         if (webkit_hit_test_result_context_is_link(ht))
629         {
630             gtk_entry_set_text(GTK_ENTRY(c->location),
631                                webkit_hit_test_result_get_link_uri(ht));
632
633             if (c->hover_uri != NULL)
634                 g_free(c->hover_uri);
635             c->hover_uri = g_strdup(webkit_hit_test_result_get_link_uri(ht));
636         }
637         else
638         {
639             gtk_entry_set_text(GTK_ENTRY(c->location),
640                                webkit_web_view_get_uri(WEBKIT_WEB_VIEW(c->web_view)));
641
642             if (c->hover_uri != NULL)
643                 g_free(c->hover_uri);
644             c->hover_uri = NULL;
645         }
646     }
647 }
648
649 gboolean
650 key_common(GtkWidget *widget, GdkEvent *event, gpointer data)
651 {
652     struct Client *c = (struct Client *)data;
653     WebKitWebContext *wc = webkit_web_view_get_context(WEBKIT_WEB_VIEW(c->web_view));
654     gchar *f;
655
656     if (event->type == GDK_KEY_PRESS)
657     {
658         if (((GdkEventKey *)event)->state & GDK_MOD1_MASK)
659         {
660             switch (((GdkEventKey *)event)->keyval)
661             {
662                 case GDK_KEY_q:  /* close window (left hand) */
663                     gtk_widget_destroy(c->win);
664                     return TRUE;
665                 case GDK_KEY_w:  /* home (left hand) */
666                     f = ensure_uri_scheme(home_uri);
667                     webkit_web_view_load_uri(WEBKIT_WEB_VIEW(c->web_view), f);
668                     g_free(f);
669                     return TRUE;
670                 case GDK_KEY_e:  /* new tab (left hand) */
671                     f = ensure_uri_scheme(home_uri);
672                     client_new(f, NULL, TRUE);
673                     g_free(f);
674                     return TRUE;
675                 case GDK_KEY_r:  /* reload (left hand) */
676                     webkit_web_view_reload_bypass_cache(WEBKIT_WEB_VIEW(
677                                                         c->web_view));
678                     return TRUE;
679                 case GDK_KEY_d:  /* download manager (left hand) */
680                     gtk_widget_show_all(dm.win);
681                     return TRUE;
682                 case GDK_KEY_2:  /* search forward (left hand) */
683                 case GDK_KEY_n:  /* search forward (maybe both hands) */
684                     search(c, 1);
685                     return TRUE;
686                 case GDK_KEY_3:  /* search backward (left hand) */
687                     search(c, -1);
688                     return TRUE;
689                 case GDK_KEY_l:  /* location (BOTH hands) */
690                     gtk_widget_grab_focus(c->location);
691                     return TRUE;
692                 case GDK_KEY_k:  /* initiate search (BOTH hands) */
693                     gtk_widget_grab_focus(c->location);
694                     gtk_entry_set_text(GTK_ENTRY(c->location), ":/");
695                     gtk_editable_set_position(GTK_EDITABLE(c->location), -1);
696                     return TRUE;
697                 case GDK_KEY_c:  /* reload trusted certs (left hand) */
698                     trust_user_certs(wc);
699                     return TRUE;
700                 case GDK_KEY_x:  /* launch external handler (left hand) */
701                     if (c->external_handler_uri != NULL)
702                         g_free(c->external_handler_uri);
703                     c->external_handler_uri = g_strdup(
704                         webkit_web_view_get_uri(WEBKIT_WEB_VIEW(c->web_view)));
705                     external_handler_run(NULL, c);
706                     return TRUE;
707             }
708         }
709         /* navigate backward (left hand) */
710         else if (((GdkEventKey *)event)->keyval == GDK_KEY_F2)
711         {
712             webkit_web_view_go_back(WEBKIT_WEB_VIEW(c->web_view));
713             return TRUE;
714         }
715         /* navigate forward (left hand) */
716         else if (((GdkEventKey *)event)->keyval == GDK_KEY_F3)
717         {
718             webkit_web_view_go_forward(WEBKIT_WEB_VIEW(c->web_view));
719             return TRUE;
720         }
721     }
722
723     return FALSE;
724 }
725
726 gboolean
727 key_downloadmanager(GtkWidget *widget, GdkEvent *event, gpointer data)
728 {
729     if (event->type == GDK_KEY_PRESS)
730     {
731         if (((GdkEventKey *)event)->state & GDK_MOD1_MASK)
732         {
733             switch (((GdkEventKey *)event)->keyval)
734             {
735                 case GDK_KEY_d:  /* close window (left hand) */
736                 case GDK_KEY_q:
737                     downloadmanager_delete(dm.win, NULL);
738                     return TRUE;
739             }
740         }
741     }
742
743     return FALSE;
744 }
745
746 gboolean
747 key_location(GtkWidget *widget, GdkEvent *event, gpointer data)
748 {
749     struct Client *c = (struct Client *)data;
750     const gchar *t;
751     gchar *f;
752
753     if (key_common(widget, event, data))
754         return TRUE;
755
756     if (event->type == GDK_KEY_PRESS)
757     {
758         switch (((GdkEventKey *)event)->keyval)
759         {
760             case GDK_KEY_KP_Enter:
761             case GDK_KEY_Return:
762                 gtk_widget_grab_focus(c->web_view);
763                 t = gtk_entry_get_text(GTK_ENTRY(c->location));
764                 if (t != NULL && t[0] == ':' && t[1] == '/')
765                 {
766                     if (search_text != NULL)
767                         g_free(search_text);
768                     search_text = g_strdup(t + 2);  /* XXX whacky */
769                     search(c, 0);
770                 }
771                 else if (!keywords_try_search(WEBKIT_WEB_VIEW(c->web_view), t))
772                 {
773                     f = ensure_uri_scheme(t);
774                     webkit_web_view_load_uri(WEBKIT_WEB_VIEW(c->web_view), f);
775                     g_free(f);
776                 }
777                 return TRUE;
778             case GDK_KEY_Escape:
779                 t = webkit_web_view_get_uri(WEBKIT_WEB_VIEW(c->web_view));
780                 gtk_entry_set_text(GTK_ENTRY(c->location),
781                                    (t == NULL ? __NAME__ : t));
782                 return TRUE;
783         }
784     }
785
786     return FALSE;
787 }
788
789 gboolean
790 key_web_view(GtkWidget *widget, GdkEvent *event, gpointer data)
791 {
792     struct Client *c = (struct Client *)data;
793     gdouble dx, dy;
794     gfloat z;
795
796     if (key_common(widget, event, data))
797         return TRUE;
798
799     if (event->type == GDK_KEY_PRESS)
800     {
801         if (((GdkEventKey *)event)->keyval == GDK_KEY_Escape)
802         {
803             webkit_web_view_stop_loading(WEBKIT_WEB_VIEW(c->web_view));
804             gtk_entry_set_progress_fraction(GTK_ENTRY(c->location), 0);
805         }
806     }
807     else if (event->type == GDK_BUTTON_PRESS)
808     {
809         switch (((GdkEventButton *)event)->button)
810         {
811             case 2:
812                 if (c->hover_uri != NULL)
813                 {
814                     client_new(c->hover_uri, NULL, TRUE);
815                     return TRUE;
816                 }
817                 break;
818             case 8:
819                 webkit_web_view_go_back(WEBKIT_WEB_VIEW(c->web_view));
820                 return TRUE;
821             case 9:
822                 webkit_web_view_go_forward(WEBKIT_WEB_VIEW(c->web_view));
823                 return TRUE;
824         }
825     }
826     else if (event->type == GDK_SCROLL)
827     {
828         if (((GdkEventScroll *)event)->state & GDK_MOD1_MASK ||
829             ((GdkEventScroll *)event)->state & GDK_CONTROL_MASK)
830         {
831             gdk_event_get_scroll_deltas(event, &dx, &dy);
832             z = webkit_web_view_get_zoom_level(WEBKIT_WEB_VIEW(c->web_view));
833             z += -dy * 0.1;
834             z = dx != 0 ? global_zoom : z;
835             webkit_web_view_set_zoom_level(WEBKIT_WEB_VIEW(c->web_view), z);
836             return TRUE;
837         }
838     }
839
840     return FALSE;
841 }
842
843 void
844 keywords_load(void)
845 {
846     GError *err = NULL;
847     GIOChannel *channel = NULL;
848     gchar *path = NULL, *buf = NULL;
849     gchar **tokens = NULL;
850
851     keywords = g_hash_table_new(g_str_hash, g_str_equal);
852
853     path = g_build_filename(g_get_user_config_dir(), __NAME__, "keywordsearch",
854                             NULL);
855     channel = g_io_channel_new_file(path, "r", &err);
856     if (channel != NULL)
857     {
858         while (g_io_channel_read_line(channel, &buf, NULL, NULL, NULL)
859                == G_IO_STATUS_NORMAL)
860         {
861             g_strstrip(buf);
862             if (buf[0] != '#')
863             {
864                 tokens = g_strsplit(buf, " ", 2);
865                 if (tokens[0] != NULL && tokens[1] != NULL)
866                     g_hash_table_insert(keywords, g_strdup(tokens[0]),
867                                         g_strdup(tokens[1]));
868                 g_strfreev(tokens);
869             }
870             g_free(buf);
871         }
872         g_io_channel_shutdown(channel, FALSE, NULL);
873     }
874     g_free(path);
875 }
876
877 gboolean
878 keywords_try_search(WebKitWebView *web_view, const gchar *t)
879 {
880     gboolean ret = FALSE;
881     gchar **tokens = NULL;
882     gchar *val = NULL, *uri = NULL;
883
884     tokens = g_strsplit(t, " ", 2);
885     if (tokens[0] != NULL && tokens[1] != NULL)
886     {
887         val = g_hash_table_lookup(keywords, tokens[0]);
888         if (val != NULL)
889         {
890             uri = g_strdup_printf((gchar *)val, tokens[1]);
891             webkit_web_view_load_uri(web_view, uri);
892             g_free(uri);
893             ret = TRUE;
894         }
895     }
896     g_strfreev(tokens);
897
898     return ret;
899 }
900
901 gboolean
902 menu_web_view(WebKitWebView *web_view, WebKitContextMenu *menu, GdkEvent *ev,
903               WebKitHitTestResult *ht, gpointer data)
904 {
905     struct Client *c = (struct Client *)data;
906     GtkAction *action = NULL;
907     WebKitContextMenuItem *mi = NULL;
908     const gchar *uri = NULL;
909
910     (void)ev;
911
912     if (webkit_hit_test_result_context_is_link(ht))
913         uri = webkit_hit_test_result_get_link_uri(ht);
914     else if (webkit_hit_test_result_context_is_image(ht))
915         uri = webkit_hit_test_result_get_image_uri(ht);
916     else if (webkit_hit_test_result_context_is_media(ht))
917         uri = webkit_hit_test_result_get_media_uri(ht);
918
919     if (uri != NULL)
920     {
921         webkit_context_menu_append(menu, webkit_context_menu_item_new_separator());
922
923         if (c->external_handler_uri != NULL)
924             g_free(c->external_handler_uri);
925         c->external_handler_uri = g_strdup(uri);
926         action = gtk_action_new("external_handler", "Open with external handler",
927                                 NULL, NULL);
928         g_signal_connect(G_OBJECT(action), "activate",
929                          G_CALLBACK(external_handler_run), data);
930         mi = webkit_context_menu_item_new(action);
931         webkit_context_menu_append(menu, mi);
932     }
933
934     /* FALSE = Show the menu. (TRUE = Don't ever show it.) */
935     return FALSE;
936 }
937
938 gboolean
939 quit_if_nothing_active(void)
940 {
941     if (clients == 0)
942     {
943         if (downloads == 0)
944         {
945             gtk_main_quit();
946             return TRUE;
947         }
948         else
949             gtk_widget_show_all(dm.win);
950     }
951
952     return FALSE;
953 }
954
955 gboolean
956 remote_msg(GIOChannel *channel, GIOCondition condition, gpointer data)
957 {
958     gchar *uri = NULL;
959
960     g_io_channel_read_line(channel, &uri, NULL, NULL, NULL);
961     if (uri)
962     {
963         g_strstrip(uri);
964         client_new(uri, NULL, TRUE);
965         g_free(uri);
966     }
967     return TRUE;
968 }
969
970 void
971 search(gpointer data, gint direction)
972 {
973     struct Client *c = (struct Client *)data;
974     WebKitWebView *web_view = WEBKIT_WEB_VIEW(c->web_view);
975     WebKitFindController *fc = webkit_web_view_get_find_controller(web_view);
976
977     if (search_text == NULL)
978         return;
979
980     switch (direction)
981     {
982         case 0:
983             webkit_find_controller_search(fc, search_text,
984                                           WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE |
985                                           WEBKIT_FIND_OPTIONS_WRAP_AROUND,
986                                           G_MAXUINT);
987             break;
988         case 1:
989             webkit_find_controller_search_next(fc);
990             break;
991         case -1:
992             webkit_find_controller_search_previous(fc);
993             break;
994     }
995 }
996
997 void
998 show_web_view(WebKitWebView *web_view, gpointer data)
999 {
1000     struct Client *c = (struct Client *)data;
1001
1002     (void)web_view;
1003
1004     gtk_widget_grab_focus(c->web_view);
1005     gtk_widget_show_all(c->win);
1006 }
1007
1008 Window
1009 tabbed_launch(void)
1010 {
1011     gint tabbed_stdout;
1012     GIOChannel *tabbed_stdout_channel;
1013     GError *err = NULL;
1014     gchar *output = NULL;
1015     char *argv[] = { "tabbed", "-c", "-d", "-p", "s1", "-n", __NAME__, NULL };
1016     Window plug_into;
1017
1018     if (!g_spawn_async_with_pipes(NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL,
1019                                   NULL, NULL, NULL, &tabbed_stdout, NULL,
1020                                   &err))
1021     {
1022         fprintf(stderr, __NAME__": Could not launch tabbed: %s\n", err->message);
1023         g_error_free(err);
1024         return 0;
1025     }
1026
1027     tabbed_stdout_channel = g_io_channel_unix_new(tabbed_stdout);
1028     if (tabbed_stdout_channel == NULL)
1029     {
1030         fprintf(stderr, __NAME__": Could open tabbed's stdout\n");
1031         return 0;
1032     }
1033     g_io_channel_read_line(tabbed_stdout_channel, &output, NULL, NULL, NULL);
1034     g_io_channel_shutdown(tabbed_stdout_channel, FALSE, NULL);
1035     if (output == NULL)
1036     {
1037         fprintf(stderr, __NAME__": Could not read XID from tabbed\n");
1038         return 0;
1039     }
1040     g_strstrip(output);
1041     plug_into = strtol(output, NULL, 16);
1042     g_free(output);
1043     if (plug_into == 0)
1044         fprintf(stderr, __NAME__": The XID from tabbed is 0\n");
1045     return plug_into;
1046 }
1047
1048 void
1049 trust_user_certs(WebKitWebContext *wc)
1050 {
1051     GTlsCertificate *cert;
1052     const gchar *basedir, *file, *absfile;
1053     GDir *dir;
1054
1055     basedir = g_build_filename(g_get_user_config_dir(), __NAME__, "certs", NULL);
1056     dir = g_dir_open(basedir, 0, NULL);
1057     if (dir != NULL)
1058     {
1059         file = g_dir_read_name(dir);
1060         while (file != NULL)
1061         {
1062             absfile = g_build_filename(g_get_user_config_dir(), __NAME__, "certs",
1063                                        file, NULL);
1064             cert = g_tls_certificate_new_from_file(absfile, NULL);
1065             if (cert == NULL)
1066                 fprintf(stderr, __NAME__": Could not load trusted cert '%s'\n", file);
1067             else
1068                 webkit_web_context_allow_tls_certificate_for_host(wc, cert, file);
1069             file = g_dir_read_name(dir);
1070         }
1071         g_dir_close(dir);
1072     }
1073 }
1074
1075
1076 int
1077 main(int argc, char **argv)
1078 {
1079     gchar *c;
1080     int opt, i;
1081
1082     gtk_init(&argc, &argv);
1083     webkit_web_context_set_process_model(webkit_web_context_get_default(),
1084         WEBKIT_PROCESS_MODEL_MULTIPLE_SECONDARY_PROCESSES);
1085
1086     grab_environment_configuration();
1087
1088     while ((opt = getopt(argc, argv, "e:CT")) != -1)
1089     {
1090         switch (opt)
1091         {
1092             case 'e':
1093                 embed = atol(optarg);
1094                 tabbed_automagic = FALSE;
1095                 break;
1096             case 'C':
1097                 cooperative_instances = FALSE;
1098                 break;
1099             case 'T':
1100                 tabbed_automagic = FALSE;
1101                 break;
1102             default:
1103                 fprintf(stderr, "Usage: "__NAME__" [OPTION]... [URI]...\n");
1104                 exit(EXIT_FAILURE);
1105         }
1106     }
1107
1108     keywords_load();
1109     if (cooperative_instances)
1110         cooperation_setup();
1111     downloadmanager_setup();
1112
1113     if (tabbed_automagic && !(cooperative_instances && !cooperative_alone))
1114         embed = tabbed_launch();
1115
1116     if (!cooperative_instances || cooperative_alone)
1117     {
1118         c = g_build_filename(g_get_user_config_dir(), __NAME__, "web_extensions",
1119                              NULL);
1120         webkit_web_context_set_web_extensions_directory(
1121             webkit_web_context_get_default(), c
1122         );
1123     }
1124
1125     if (optind >= argc)
1126         client_new(home_uri, NULL, TRUE);
1127     else
1128     {
1129         for (i = optind; i < argc; i++)
1130             client_new(argv[i], NULL, TRUE);
1131     }
1132
1133     if (!cooperative_instances || cooperative_alone)
1134         gtk_main();
1135     exit(EXIT_SUCCESS);
1136 }