]> git.armaanb.net Git - chorizo.git/blob - src/browser.c
Persist clipboard after program closes
[chorizo.git] / src / browser.c
1 #include <fcntl.h>
2 #include <sys/stat.h>
3 #include <webkit2/webkit2.h>
4
5 #include "downloads.h"
6
7 void client_destroy(GtkWidget *, gpointer);
8 WebKitWebView *client_new(const gchar *, WebKitWebView *, gboolean, gboolean);
9 WebKitWebView *client_new_request(WebKitWebView *, WebKitNavigationAction *,
10                                   gpointer);
11 void cooperation_setup(void);
12 void changed_load_progress(GObject *, GParamSpec *, gpointer);
13 void changed_favicon(GObject *, GParamSpec *, gpointer);
14 void changed_title(GObject *, GParamSpec *, gpointer);
15 void changed_uri(GObject *, GParamSpec *, gpointer);
16 gboolean crashed_web_view(WebKitWebView *, gpointer);
17 gboolean decide_policy(WebKitWebView *, WebKitPolicyDecision *,
18                        WebKitPolicyDecisionType, gpointer);
19 gchar *ensure_uri_scheme(const gchar *);
20 void grab_environment_configuration(void);
21 void grab_feeds_finished(GObject *, GAsyncResult *, gpointer);
22 void hover_web_view(WebKitWebView *, WebKitHitTestResult *, guint, gpointer);
23 void icon_location(GtkEntry *, GtkEntryIconPosition, GdkEvent *, gpointer);
24 void init_default_web_context(void);
25 gboolean key_common(GtkWidget *, GdkEvent *, gpointer);
26 gboolean key_location(GtkWidget *, GdkEvent *, gpointer);
27 gboolean key_tablabel(GtkWidget *, GdkEvent *, gpointer);
28 gboolean key_web_view(GtkWidget *, GdkEvent *, gpointer);
29 void mainwindow_setup(void);
30 void mainwindow_title(gint);
31 void notebook_switch_page(GtkNotebook *, GtkWidget *, guint, gpointer);
32 gboolean remote_msg(GIOChannel *, GIOCondition, gpointer);
33 void run_user_scripts(WebKitWebView *);
34 void search(gpointer, gint);
35 void show_web_view(WebKitWebView *, gpointer);
36 void trust_user_certs(WebKitWebContext *);
37 GKeyFile *get_ini(void);
38 GKeyFile *config;
39
40 struct Client {
41     GtkWidget *imgbutton;
42     GtkWidget *jsbutton;
43     GtkWidget *location;
44     GtkWidget *tabicon;
45     GtkWidget *tablabel;
46     GtkWidget *vbox;
47     GtkWidget *web_view;
48     WebKitSettings *settings;
49     gboolean focus_new_tab;
50     gchar *external_handler_uri;
51     gchar *feed_html;
52     gchar *hover_uri;
53 };
54
55 struct Configuration {
56     WebKitCookieAcceptPolicy cookie_policy;
57     const gchar *accepted_language[2];
58     gboolean cooperative_alone;
59     gboolean cooperative_instances;
60     gboolean enable_console_to_stdout;
61     gboolean images_disabled;
62     gboolean javascript_disabled;
63     gboolean private;
64     gboolean spellcheck_disabled;
65     gchar *default_uri;
66     gchar *download_dir;
67     gchar *fifo_suffix;
68     gchar *history_file;
69     gchar *home_uri;
70     gchar *search_engine;
71     gchar *spellcheck_language;
72     gchar *user_agent;
73     gdouble global_zoom;
74     gint scroll_lines;
75     gint tab_width_chars;
76 } cfg;
77
78 struct MainWindow {
79     GtkWidget *win;
80     GtkWidget *notebook;
81 } mw;
82
83 gint clients = 0;
84 struct Client **client_arr;
85
86 int cooperative_pipe_fp = 0;
87 gchar *search_text;
88
89 void
90 togglejs(GtkButton *jsbutton, gpointer data) {
91     struct Client *c = (struct Client *)data;
92     webkit_settings_set_enable_javascript(
93         c->settings, !webkit_settings_get_enable_javascript(c->settings));
94     webkit_web_view_set_settings(WEBKIT_WEB_VIEW(c->web_view), c->settings);
95 }
96
97 void
98 toggleimg(GtkButton *imgbutton, gpointer data) {
99     struct Client *c = (struct Client *)data;
100     webkit_settings_set_auto_load_images(
101         c->settings, !webkit_settings_get_auto_load_images(c->settings));
102     webkit_web_view_set_settings(WEBKIT_WEB_VIEW(c->web_view), c->settings);
103 }
104
105 void
106 client_destroy(GtkWidget *widget, gpointer data) {
107     struct Client *c = (struct Client *)data;
108     gint idx;
109
110     g_signal_handlers_disconnect_by_func(G_OBJECT(c->web_view),
111                                          changed_load_progress, c);
112
113     idx = gtk_notebook_page_num(GTK_NOTEBOOK(mw.notebook), c->vbox);
114     if (idx == -1)
115         fprintf(stderr, __NAME__ ": Tab index was -1, bamboozled\n");
116     else
117         gtk_notebook_remove_page(GTK_NOTEBOOK(mw.notebook), idx);
118
119     free(c);
120     clients--;
121
122     quit_if_nothing_active();
123 }
124
125 void
126 set_uri(const char *uri, struct Client *c) {
127     if (!gtk_widget_is_focus(c->location)) {
128         gtk_entry_set_text(GTK_ENTRY(c->location), uri);
129     }
130 }
131
132 WebKitWebView *
133 client_new(const gchar *uri, WebKitWebView *related_wv, gboolean show,
134            gboolean focus_tab) {
135     struct Client *c;
136     gchar *f;
137     GtkWidget *evbox, *tabbox;
138
139     if (uri != NULL && cfg.cooperative_instances && !cfg.cooperative_alone) {
140         f = ensure_uri_scheme(uri);
141         write(cooperative_pipe_fp, f, strlen(f));
142         write(cooperative_pipe_fp, "\n", 1);
143         g_free(f);
144         return NULL;
145     }
146
147     c = calloc(1, sizeof(struct Client));
148     if (!c) {
149         fprintf(stderr, __NAME__ ": fatal: calloc failed\n");
150         exit(EXIT_FAILURE);
151     }
152
153     c->focus_new_tab = focus_tab;
154
155     if (related_wv == NULL) {
156         WebKitUserContentManager *ucm = webkit_user_content_manager_new();
157         WebKitUserScript *wkscript;
158         WebKitUserStyleSheet *wkstyle;
159         gchar *path = NULL, *source, *base;
160         const gchar *entry = NULL;
161         GDir *dir = NULL;
162
163         base = g_build_filename(g_get_user_data_dir(), __NAME__, "user-scripts",
164                                 NULL);
165         dir = g_dir_open(base, 0, NULL);
166         if (dir != NULL) {
167             while ((entry = g_dir_read_name(dir)) != NULL) {
168                 path = g_build_filename(base, entry, NULL);
169                 if (g_str_has_suffix(path, ".js")) {
170                     g_file_get_contents(path, &source, NULL, NULL);
171                     wkscript = webkit_user_script_new(
172                         source, WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
173                         WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, NULL,
174                         NULL);
175                     webkit_user_content_manager_add_script(ucm, wkscript);
176                     webkit_user_script_unref(wkscript);
177                 }
178                 g_free(path);
179                 g_free(source);
180             }
181             g_dir_close(dir);
182         }
183
184         base = g_build_filename(g_get_user_data_dir(), __NAME__, "user-styles",
185                                 NULL);
186         dir = g_dir_open(base, 0, NULL);
187         if (dir != NULL) {
188             while ((entry = g_dir_read_name(dir)) != NULL) {
189                 path = g_build_filename(base, entry, NULL);
190                 if (g_str_has_suffix(path, ".css")) {
191                     g_file_get_contents(path, &source, NULL, NULL);
192                     wkstyle = webkit_user_style_sheet_new(
193                         source, WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
194                         WEBKIT_USER_STYLE_LEVEL_USER, NULL, NULL);
195                     webkit_user_content_manager_add_style_sheet(ucm, wkstyle);
196                     webkit_user_style_sheet_unref(wkstyle);
197                 }
198                 g_free(path);
199                 g_free(source);
200             }
201             g_dir_close(dir);
202         }
203
204         g_free(base);
205
206         c->web_view = webkit_web_view_new_with_user_content_manager(ucm);
207     } else {
208         c->web_view = webkit_web_view_new_with_related_view(related_wv);
209     }
210
211     c->settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(c->web_view));
212     webkit_web_view_set_zoom_level(WEBKIT_WEB_VIEW(c->web_view),
213                                    cfg.global_zoom);
214     webkit_settings_set_enable_javascript(c->settings,
215                                           !cfg.javascript_disabled);
216     webkit_settings_set_auto_load_images(c->settings, !cfg.images_disabled);
217     g_signal_connect(G_OBJECT(c->web_view), "notify::favicon",
218                      G_CALLBACK(changed_favicon), c);
219     g_signal_connect(G_OBJECT(c->web_view), "notify::title",
220                      G_CALLBACK(changed_title), c);
221     g_signal_connect(G_OBJECT(c->web_view), "notify::uri",
222                      G_CALLBACK(changed_uri), c);
223     g_signal_connect(G_OBJECT(c->web_view), "notify::estimated-load-progress",
224                      G_CALLBACK(changed_load_progress), c);
225     g_signal_connect(G_OBJECT(c->web_view), "create",
226                      G_CALLBACK(client_new_request), NULL);
227     g_signal_connect(G_OBJECT(c->web_view), "close", G_CALLBACK(client_destroy),
228                      c);
229     g_signal_connect(G_OBJECT(c->web_view), "decide-policy",
230                      G_CALLBACK(decide_policy), NULL);
231     g_signal_connect(G_OBJECT(c->web_view), "key-press-event",
232                      G_CALLBACK(key_web_view), c);
233     g_signal_connect(G_OBJECT(c->web_view), "button-release-event",
234                      G_CALLBACK(key_web_view), c);
235     g_signal_connect(G_OBJECT(c->web_view), "scroll-event",
236                      G_CALLBACK(key_web_view), c);
237     g_signal_connect(G_OBJECT(c->web_view), "mouse-target-changed",
238                      G_CALLBACK(hover_web_view), c);
239     g_signal_connect(G_OBJECT(c->web_view), "web-process-crashed",
240                      G_CALLBACK(crashed_web_view), c);
241
242     if (cfg.user_agent != NULL) {
243         g_object_set(c->settings, "user-agent", cfg.user_agent, NULL);
244     }
245
246     if (cfg.enable_console_to_stdout)
247         webkit_settings_set_enable_write_console_messages_to_stdout(c->settings,
248                                                                     TRUE);
249
250     webkit_settings_set_enable_developer_extras(c->settings, TRUE);
251
252     GtkWidget *locbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
253
254     c->jsbutton = gtk_toggle_button_new_with_label("JS");
255     gtk_widget_set_tooltip_text(c->jsbutton, "Toggle JavaScript execution");
256     gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(c->jsbutton),
257                                  !cfg.javascript_disabled);
258     g_signal_connect(G_OBJECT(c->jsbutton), "toggled", G_CALLBACK(togglejs), c);
259
260     c->imgbutton = gtk_toggle_button_new_with_label("IMG");
261     gtk_widget_set_tooltip_text(c->imgbutton, "Toggle image loading");
262     gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(c->imgbutton),
263                                  !cfg.images_disabled);
264     g_signal_connect(G_OBJECT(c->imgbutton), "toggled", G_CALLBACK(toggleimg),
265                      c);
266
267     c->location = gtk_entry_new();
268     gtk_box_pack_start(GTK_BOX(locbox), c->location, TRUE, TRUE, 0);
269
270     if (cfg.private) {
271         GtkWidget *privindicator = gtk_label_new("Private mode");
272         gtk_widget_set_tooltip_text(
273             privindicator, "You are in private mode. No history, caches, or "
274                            "cookies will be saved beyond this session.");
275         gtk_box_pack_end(GTK_BOX(locbox), privindicator, FALSE, FALSE, 5);
276     }
277
278     gtk_box_pack_start(GTK_BOX(locbox), c->jsbutton, FALSE, FALSE, 5);
279     gtk_box_pack_start(GTK_BOX(locbox), c->imgbutton, FALSE, FALSE, 0);
280
281     g_signal_connect(G_OBJECT(c->location), "key-press-event",
282                      G_CALLBACK(key_location), c);
283     g_signal_connect(G_OBJECT(c->location), "icon-release",
284                      G_CALLBACK(icon_location), c);
285     /* XXX This is a workaround. Setting this to NULL (which is done in
286      * grab_feeds_finished() if no feed has been detected) adds a little
287      * padding left of the text. Not sure why. The point of this call
288      * right here is to have that padding right from the start. This
289      * avoids a graphical artifact. */
290     gtk_entry_set_icon_from_icon_name(GTK_ENTRY(c->location),
291                                       GTK_ENTRY_ICON_SECONDARY, NULL);
292
293     c->vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
294     gtk_box_pack_start(GTK_BOX(c->vbox), locbox, FALSE, FALSE, 0);
295     gtk_box_pack_start(GTK_BOX(c->vbox), c->web_view, TRUE, TRUE, 0);
296     gtk_container_set_focus_child(GTK_CONTAINER(c->vbox), c->web_view);
297
298     c->tabicon =
299         gtk_image_new_from_icon_name("text-html", GTK_ICON_SIZE_SMALL_TOOLBAR);
300
301     c->tablabel = gtk_label_new(__NAME__);
302     gtk_label_set_ellipsize(GTK_LABEL(c->tablabel), PANGO_ELLIPSIZE_END);
303     gtk_label_set_width_chars(GTK_LABEL(c->tablabel), cfg.tab_width_chars);
304     gtk_widget_set_has_tooltip(c->tablabel, TRUE);
305
306     /* XXX I don't own a HiDPI screen, so I don't know if scale_factor
307      * does the right thing. */
308     tabbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL,
309                          5 * gtk_widget_get_scale_factor(mw.win));
310     gtk_box_pack_start(GTK_BOX(tabbox), c->tabicon, FALSE, FALSE, 0);
311     gtk_box_pack_start(GTK_BOX(tabbox), c->tablabel, TRUE, TRUE, 0);
312
313     evbox = gtk_event_box_new();
314     gtk_container_add(GTK_CONTAINER(evbox), tabbox);
315     g_signal_connect(G_OBJECT(evbox), "button-release-event",
316                      G_CALLBACK(key_tablabel), c);
317
318     gtk_widget_add_events(evbox, GDK_SCROLL_MASK);
319     g_signal_connect(G_OBJECT(evbox), "scroll-event", G_CALLBACK(key_tablabel),
320                      c);
321
322     // For easy access, store a reference to our label.
323     g_object_set_data(G_OBJECT(evbox), "chorizo-tab-label", c->tablabel);
324
325     /* This only shows the event box and the label inside, nothing else.
326      * Needed because the evbox/label is "internal" to the notebook and
327      * not part of the normal "widget tree" (IIUC). */
328     gtk_widget_show_all(evbox);
329
330     int page = gtk_notebook_get_current_page(GTK_NOTEBOOK(mw.notebook)) + 1;
331     gtk_notebook_insert_page(GTK_NOTEBOOK(mw.notebook), c->vbox, evbox, page);
332     gtk_notebook_set_tab_reorderable(GTK_NOTEBOOK(mw.notebook), c->vbox, TRUE);
333
334     if (show)
335         show_web_view(NULL, c);
336     else
337         g_signal_connect(G_OBJECT(c->web_view), "ready-to-show",
338                          G_CALLBACK(show_web_view), c);
339
340     if (uri != NULL) {
341         f = ensure_uri_scheme(uri);
342         webkit_web_view_load_uri(WEBKIT_WEB_VIEW(c->web_view), f);
343         g_free(f);
344     }
345
346     set_uri(uri, c);
347
348     clients++;
349     client_arr = realloc(client_arr, (clients + 1) * sizeof(client_arr[0]));
350     client_arr[clients] = c;
351
352     return WEBKIT_WEB_VIEW(c->web_view);
353 }
354
355 WebKitWebView *
356 client_new_request(WebKitWebView *web_view,
357                    WebKitNavigationAction *navigation_action, gpointer data) {
358     return client_new(NULL, web_view, FALSE, FALSE);
359 }
360
361 void
362 cooperation_setup(void) {
363     GIOChannel *towatch;
364     gchar *fifofilename, *fifopath;
365
366     gchar *priv = (cfg.private) ? "-private" : "";
367     fifofilename =
368         g_strdup_printf("%s%s%s-%s", __NAME__, priv, ".fifo", cfg.fifo_suffix);
369     fifopath = g_build_filename(g_get_user_runtime_dir(), fifofilename, NULL);
370     g_free(fifofilename);
371
372     if (!g_file_test(fifopath, G_FILE_TEST_EXISTS))
373         mkfifo(fifopath, 0600);
374
375     cooperative_pipe_fp = open(fifopath, O_WRONLY | O_NONBLOCK);
376     if (!cooperative_pipe_fp) {
377         fprintf(stderr, __NAME__ ": Can't open FIFO at all.\n");
378     } else {
379         if (write(cooperative_pipe_fp, "", 0) == -1) {
380             /* Could not do an empty write to the FIFO which means there's
381              * no one listening. */
382             close(cooperative_pipe_fp);
383             towatch = g_io_channel_new_file(fifopath, "r+", NULL);
384             g_io_add_watch(towatch, G_IO_IN, (GIOFunc)remote_msg, NULL);
385         } else {
386             cfg.cooperative_alone = FALSE;
387         }
388     }
389
390     g_free(fifopath);
391 }
392
393 void
394 changed_load_progress(GObject *obj, GParamSpec *pspec, gpointer data) {
395     struct Client *c = (struct Client *)data;
396     gdouble p;
397     gchar *grab_feeds =
398         "a = document.querySelectorAll('"
399         "    html > head > "
400         "link[rel=\"alternate\"][href][type=\"application/atom+xml\"],"
401         "    html > head > "
402         "link[rel=\"alternate\"][href][type=\"application/rss+xml\"]"
403         "');"
404         "if (a.length == 0)"
405         "    null;"
406         "else {"
407         "    out = '';"
408         "    for (i = 0; i < a.length; i++) {"
409         "        url = encodeURIComponent(a[i].href);"
410         "        if ('title' in a[i] && a[i].title != '')"
411         "            title = encodeURIComponent(a[i].title);"
412         "        else"
413         "            title = url;"
414         "        out += '<li><a href=\"' + url + '\">' + title + "
415         "'</a></li>';"
416         "    }"
417         "    out;"
418         "}";
419
420     p = webkit_web_view_get_estimated_load_progress(
421         WEBKIT_WEB_VIEW(c->web_view));
422     if (p == 1) {
423         p = 0;
424
425         /* The page has loaded fully. We now run the short JavaScript
426          * snippet above that operates on the DOM. It tries to grab all
427          * occurences of <link rel="alternate" ...>, i.e. RSS/Atom feed
428          * references. */
429         webkit_web_view_run_javascript(WEBKIT_WEB_VIEW(c->web_view), grab_feeds,
430                                        NULL, grab_feeds_finished, c);
431     }
432     gtk_entry_set_progress_fraction(GTK_ENTRY(c->location), p);
433 }
434
435 void
436 changed_favicon(GObject *obj, GParamSpec *pspec, gpointer data) {
437     struct Client *c = (struct Client *)data;
438     cairo_surface_t *f;
439     int w, h, w_should, h_should;
440     GdkPixbuf *pb, *pb_scaled;
441
442     f = webkit_web_view_get_favicon(WEBKIT_WEB_VIEW(c->web_view));
443     if (f == NULL) {
444         gtk_image_set_from_icon_name(GTK_IMAGE(c->tabicon), "text-html",
445                                      GTK_ICON_SIZE_SMALL_TOOLBAR);
446     } else {
447         w = cairo_image_surface_get_width(f);
448         h = cairo_image_surface_get_height(f);
449         pb = gdk_pixbuf_get_from_surface(f, 0, 0, w, h);
450         if (pb != NULL) {
451             w_should = 16 * gtk_widget_get_scale_factor(c->tabicon);
452             h_should = 16 * gtk_widget_get_scale_factor(c->tabicon);
453             pb_scaled = gdk_pixbuf_scale_simple(pb, w_should, h_should,
454                                                 GDK_INTERP_BILINEAR);
455             gtk_image_set_from_pixbuf(GTK_IMAGE(c->tabicon), pb_scaled);
456
457             g_object_unref(pb_scaled);
458             g_object_unref(pb);
459         }
460     }
461 }
462
463 void
464 changed_title(GObject *obj, GParamSpec *pspec, gpointer data) {
465     const gchar *t, *u;
466     struct Client *c = (struct Client *)data;
467
468     u = webkit_web_view_get_uri(WEBKIT_WEB_VIEW(c->web_view));
469     t = webkit_web_view_get_title(WEBKIT_WEB_VIEW(c->web_view));
470
471     u = u == NULL ? __NAME__ : u;
472     u = u[0] == 0 ? __NAME__ : u;
473
474     t = t == NULL ? u : t;
475     t = t[0] == 0 ? u : t;
476
477     gchar *name = malloc(strlen(t) + 4);
478     gboolean mute = webkit_web_view_get_is_muted(WEBKIT_WEB_VIEW(c->web_view));
479     gchar *muted = (mute) ? "[m] " : "";
480     sprintf(name, "%s%s", muted, t);
481     gtk_label_set_text(GTK_LABEL(c->tablabel), name);
482     g_free(name);
483
484     gtk_widget_set_tooltip_text(c->tablabel, t);
485     mainwindow_title(gtk_notebook_get_current_page(GTK_NOTEBOOK(mw.notebook)));
486 }
487
488 void
489 changed_uri(GObject *obj, GParamSpec *pspec, gpointer data) {
490     const gchar *t;
491     struct Client *c = (struct Client *)data;
492     FILE *fp;
493
494     t = webkit_web_view_get_uri(WEBKIT_WEB_VIEW(c->web_view));
495
496     /* When a web process crashes, we get a "notify::uri" signal, but we
497      * can no longer read a meaningful URI. It's just an empty string
498      * now. Not updating the location bar in this scenario is important,
499      * because we would override the "WEB PROCESS CRASHED" message. */
500     if (t != NULL && strlen(t) > 0) {
501         set_uri(t, c);
502
503         if (cfg.history_file != NULL && !cfg.private) {
504             fp = fopen(cfg.history_file, "a");
505             if (fp != NULL) {
506                 fprintf(fp, "%s\n", t);
507                 fclose(fp);
508             } else {
509                 perror(__NAME__ ": Error opening history file");
510             }
511         }
512     }
513 }
514
515 gboolean
516 crashed_web_view(WebKitWebView *web_view, gpointer data) {
517     gchar *t;
518     struct Client *c = (struct Client *)data;
519
520     t = g_strdup_printf("WEB PROCESS CRASHED: %s",
521                         webkit_web_view_get_uri(WEBKIT_WEB_VIEW(web_view)));
522     gtk_entry_set_text(GTK_ENTRY(c->location), t);
523     g_free(t);
524
525     return TRUE;
526 }
527
528 gboolean
529 decide_policy(WebKitWebView *web_view, WebKitPolicyDecision *decision,
530               WebKitPolicyDecisionType type, gpointer data) {
531     WebKitResponsePolicyDecision *r;
532
533     switch (type) {
534     case WEBKIT_POLICY_DECISION_TYPE_RESPONSE:
535         r = WEBKIT_RESPONSE_POLICY_DECISION(decision);
536         if (!webkit_response_policy_decision_is_mime_type_supported(r))
537             webkit_policy_decision_download(decision);
538         else
539             webkit_policy_decision_use(decision);
540         break;
541     default:
542         // Use whatever default there is.
543         return FALSE;
544     }
545     return TRUE;
546 }
547
548 gchar *
549 ensure_uri_scheme(const gchar *t) {
550     gchar *f, *fabs;
551
552     f = g_ascii_strdown(t, -1);
553     if (!g_str_has_prefix(f, "http:") && !g_str_has_prefix(f, "https:") &&
554         !g_str_has_prefix(f, "file:") && !g_str_has_prefix(f, "about:") &&
555         !g_str_has_prefix(f, "data:") && !g_str_has_prefix(f, "webkit:")) {
556         g_free(f);
557         fabs = realpath(t, NULL);
558         if (fabs != NULL) {
559             f = g_strdup_printf("file://%s", fabs);
560             free(fabs);
561         } else {
562             f = g_strdup_printf("http://%s", t);
563         }
564         return f;
565     } else
566         return g_strdup(t);
567 }
568
569 void
570 get_config(void) {
571     cfg.accepted_language[0] = NULL;
572     cfg.accepted_language[1] = NULL;
573     cfg.cooperative_alone = TRUE;
574     cfg.cooperative_instances = TRUE;
575     cfg.fifo_suffix = "main";
576
577     const char *e = g_getenv(__NAME_UPPERCASE__ "_FIFO_SUFFIX");
578     if (e != NULL)
579         cfg.fifo_suffix = g_strdup(e);
580
581     char *xdg_down = getenv("XDG_DOWNLOAD_DIR");
582     cfg.download_dir = (xdg_down) ? xdg_down : "/var/tmp";
583
584     config = get_ini();
585     cfg.accepted_language[0] =
586         g_key_file_get_string(config, "browser", "accepted_language", NULL);
587     cfg.history_file =
588         g_key_file_get_string(config, "browser", "history_file", NULL);
589     cfg.home_uri = g_key_file_get_string(config, "browser", "homepage", NULL);
590     cfg.home_uri = (cfg.home_uri) ? cfg.home_uri : "about:blank";
591     cfg.enable_console_to_stdout =
592         g_key_file_get_boolean(config, "browser", "console_to_stdout", NULL);
593     cfg.user_agent =
594         g_key_file_get_string(config, "browser", "user_agent", NULL);
595     cfg.javascript_disabled =
596         g_key_file_get_boolean(config, "browser", "javascript_disabled", NULL);
597     cfg.images_disabled =
598         g_key_file_get_boolean(config, "browser", "images_disabled", NULL);
599     char *input_cookie_policy =
600         g_key_file_get_string(config, "browser", "cookie_policy", NULL);
601     cfg.cookie_policy = WEBKIT_COOKIE_POLICY_ACCEPT_NO_THIRD_PARTY;
602     if (input_cookie_policy) {
603         if (strcmp(input_cookie_policy, "all") == 0) {
604             cfg.cookie_policy = WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS;
605         } else if (strcmp(input_cookie_policy, "none") == 0) {
606             cfg.cookie_policy = WEBKIT_COOKIE_POLICY_ACCEPT_NEVER;
607         }
608     }
609
610     cfg.default_uri = g_key_file_get_string(config, "ui", "default_uri", NULL);
611     cfg.default_uri = (cfg.default_uri) ? cfg.default_uri : "https://";
612     cfg.tab_width_chars =
613         g_key_file_get_integer(config, "ui", "tab_width", NULL);
614     cfg.tab_width_chars = (cfg.tab_width_chars) ? cfg.tab_width_chars : 20;
615     cfg.global_zoom = g_key_file_get_double(config, "ui", "zoom_level", NULL);
616     cfg.global_zoom = (cfg.global_zoom) ? cfg.global_zoom : 1.0;
617     cfg.search_engine =
618         g_key_file_get_string(config, "ui", "search_engine", NULL);
619     cfg.search_engine =
620         (cfg.search_engine) ? cfg.search_engine : "https://duckduckgo.com?q=";
621     cfg.spellcheck_disabled =
622         g_key_file_get_boolean(config, "ui", "spellcheck_disabled", NULL);
623     cfg.spellcheck_language =
624         g_key_file_get_string(config, "ui", "spellcheck_language", NULL);
625     cfg.spellcheck_language =
626         (cfg.spellcheck_language) ? cfg.spellcheck_language : "en_US";
627     cfg.scroll_lines =
628         g_key_file_get_integer(config, "ui", "scroll_lines", NULL);
629     cfg.scroll_lines = (cfg.scroll_lines) ? cfg.scroll_lines : 3;
630 }
631
632 void
633 grab_feeds_finished(GObject *object, GAsyncResult *result, gpointer data) {
634     struct Client *c = (struct Client *)data;
635     WebKitJavascriptResult *js_result;
636     JSCValue *value;
637     JSCException *exception;
638     GError *err = NULL;
639     gchar *str_value;
640
641     g_free(c->feed_html);
642     c->feed_html = NULL;
643
644     /* This was taken almost verbatim from the example in WebKit's
645      * documentation:
646      *
647      * https://webkitgtk.org/reference/webkit2gtk/stable/WebKitWebView.html
648      */
649
650     js_result = webkit_web_view_run_javascript_finish(WEBKIT_WEB_VIEW(object),
651                                                       result, &err);
652     if (!js_result) {
653         fprintf(stderr, __NAME__ ": Error running javascript: %s\n",
654                 err->message);
655         g_error_free(err);
656         return;
657     }
658
659     value = webkit_javascript_result_get_js_value(js_result);
660     if (jsc_value_is_string(value)) {
661         str_value = jsc_value_to_string(value);
662         exception = jsc_context_get_exception(jsc_value_get_context(value));
663         if (exception != NULL) {
664             fprintf(stderr, __NAME__ ": Error running javascript: %s\n",
665                     jsc_exception_get_message(exception));
666         } else {
667             c->feed_html = str_value;
668         }
669
670         gtk_entry_set_icon_from_icon_name(GTK_ENTRY(c->location),
671                                           GTK_ENTRY_ICON_SECONDARY,
672                                           "application-rss+xml-symbolic");
673         gtk_entry_set_icon_activatable(GTK_ENTRY(c->location),
674                                        GTK_ENTRY_ICON_PRIMARY, TRUE);
675     } else {
676         gtk_entry_set_icon_from_icon_name(GTK_ENTRY(c->location),
677                                           GTK_ENTRY_ICON_SECONDARY, NULL);
678     }
679
680     webkit_javascript_result_unref(js_result);
681 }
682
683 void
684 hover_web_view(WebKitWebView *web_view, WebKitHitTestResult *ht,
685                guint modifiers, gpointer data) {
686     struct Client *c = (struct Client *)data;
687     const char *to_show;
688
689     g_free(c->hover_uri);
690
691     if (webkit_hit_test_result_context_is_link(ht)) {
692         to_show = webkit_hit_test_result_get_link_uri(ht);
693         c->hover_uri = g_strdup(to_show);
694     } else {
695         to_show = webkit_web_view_get_uri(WEBKIT_WEB_VIEW(c->web_view));
696         c->hover_uri = NULL;
697     }
698
699     if (!gtk_widget_is_focus(c->location))
700         set_uri(to_show, c);
701 }
702
703 void
704 icon_location(GtkEntry *entry, GtkEntryIconPosition icon_pos, GdkEvent *event,
705               gpointer data) {
706     struct Client *c = (struct Client *)data;
707     gchar *d;
708     gchar *data_template = "data:text/html,"
709                            "<!DOCTYPE html>"
710                            "<html>"
711                            "    <head>"
712                            "        <meta charset=\"UTF-8\">"
713                            "        <title>Feeds</title>"
714                            "    </head>"
715                            "    <body>"
716                            "        <p>Feeds found on this page:</p>"
717                            "        <ul>"
718                            "        %s"
719                            "        </ul>"
720                            "    </body>"
721                            "</html>";
722
723     if (c->feed_html != NULL) {
724         /* What we're actually trying to do is show a simple HTML page
725          * that lists all the feeds on the current page. The function
726          * webkit_web_view_load_html() looks like the proper way to do
727          * that. Sad thing is, it doesn't create a history entry, but
728          * instead simply replaces the content of the current page. This
729          * is not what we want.
730          *
731          * RFC 2397 [0] defines the data URI scheme [1]. We abuse this
732          * mechanism to show my custom HTML snippet *and* create a
733          * history entry.
734          *
735          * [0]: https://tools.ietf.org/html/rfc2397
736          * [1]: https://en.wikipedia.org/wiki/Data_URI_scheme */
737         d = g_strdup_printf(data_template, c->feed_html);
738         webkit_web_view_load_uri(WEBKIT_WEB_VIEW(c->web_view), d);
739         g_free(d);
740     }
741 }
742
743 void
744 init_default_web_context(void) {
745     gchar *p;
746     WebKitWebContext *wc;
747     WebKitCookieManager *cm;
748
749     wc = (cfg.private) ? webkit_web_context_new_ephemeral()
750                        : webkit_web_context_get_default();
751
752     p = g_build_filename(g_get_user_config_dir(), __NAME__, "adblock", NULL);
753     webkit_web_context_set_sandbox_enabled(wc, TRUE);
754     webkit_web_context_add_path_to_sandbox(wc, p, TRUE);
755     g_free(p);
756
757     WebKitProcessModel model =
758         WEBKIT_PROCESS_MODEL_MULTIPLE_SECONDARY_PROCESSES;
759     webkit_web_context_set_process_model(wc, model);
760
761     p = g_build_filename(g_get_user_data_dir(), __NAME__, "web_extensions",
762                          NULL);
763     webkit_web_context_set_web_extensions_directory(wc, p);
764     g_free(p);
765
766     if (cfg.accepted_language[0] != NULL)
767         webkit_web_context_set_preferred_languages(wc, cfg.accepted_language);
768
769     g_signal_connect(G_OBJECT(wc), "download-started",
770                      G_CALLBACK(download_start), cfg.download_dir);
771
772     trust_user_certs(wc);
773
774     cm = webkit_web_context_get_cookie_manager(wc);
775     webkit_cookie_manager_set_accept_policy(cm, cfg.cookie_policy);
776
777     if (!cfg.private) {
778         webkit_web_context_set_favicon_database_directory(wc, NULL);
779
780         gchar *fname = g_build_filename("/", g_get_user_data_dir(), __NAME__,
781                                         "cookies.db", NULL);
782         WebKitCookiePersistentStorage type =
783             WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE;
784         webkit_cookie_manager_set_persistent_storage(cm, fname, type);
785         g_free(fname);
786     }
787
788     const gchar *const languages[2] = {(const gchar *)cfg.spellcheck_language,
789                                        NULL};
790     webkit_web_context_set_spell_checking_languages(wc, languages);
791     webkit_web_context_set_spell_checking_enabled(wc, !cfg.spellcheck_disabled);
792 }
793
794 void
795 search(gpointer data, gint direction) {
796     struct Client *c = (struct Client *)data;
797     WebKitWebView *web_view = WEBKIT_WEB_VIEW(c->web_view);
798     WebKitFindController *fc = webkit_web_view_get_find_controller(web_view);
799
800     if (search_text == NULL)
801         return;
802
803     switch (direction) {
804     case 0:
805         webkit_find_controller_search(fc, search_text,
806                                       WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE |
807                                           WEBKIT_FIND_OPTIONS_WRAP_AROUND,
808                                       G_MAXUINT);
809         break;
810     case 1:
811         webkit_find_controller_search_next(fc);
812         break;
813     case -1:
814         webkit_find_controller_search_previous(fc);
815         break;
816     case 2:
817         webkit_find_controller_search_finish(fc);
818         break;
819     }
820 }
821
822 void
823 search_init(struct Client *c, int direction) {
824     gtk_widget_grab_focus(c->location);
825     const gchar *contents = gtk_entry_get_text(GTK_ENTRY(c->location));
826     if (strcspn(contents, "s/")) {
827         gtk_entry_set_text(GTK_ENTRY(c->location), "s/");
828     }
829     gtk_editable_set_position(GTK_EDITABLE(c->location), -1);
830     search(c, 0);
831     search(c, -1);
832     search(c, direction);
833 }
834
835 int
836 def_key(char *key, unsigned int def) {
837     char *conf = g_key_file_get_string(config, "keybindings", key, NULL);
838     return (conf) ? gdk_keyval_from_name((conf) ? conf : NULL) : def;
839 }
840
841 gboolean
842 key_common(GtkWidget *widget, GdkEvent *event, gpointer data) {
843     struct Client *c = (struct Client *)data;
844     gdouble now;
845     gchar *f;
846
847     if (event->type == GDK_KEY_PRESS) {
848         if (((GdkEventKey *)event)->state & GDK_CONTROL_MASK) {
849             const char *uri =
850                 webkit_web_view_get_uri(WEBKIT_WEB_VIEW(c->web_view));
851             int key = ((GdkEventKey *)event)->keyval;
852             if (def_key("download_manager", GDK_KEY_y) == key) {
853                 downloadmanager_show();
854                 return TRUE;
855             } else if (def_key("history_back", GDK_KEY_h) == key) {
856                 webkit_web_view_go_back(WEBKIT_WEB_VIEW(c->web_view));
857                 return TRUE;
858             } else if (def_key("history_forwards", GDK_KEY_l) == key) {
859                 webkit_web_view_go_forward(WEBKIT_WEB_VIEW(c->web_view));
860                 return TRUE;
861             } else if (def_key("location", GDK_KEY_t) == key) {
862                 gtk_widget_grab_focus(c->location);
863                 const char *goal = (strcmp(cfg.home_uri, uri) == 0 || !uri)
864                                        ? cfg.default_uri
865                                        : uri;
866                 gtk_entry_set_text(GTK_ENTRY(c->location), goal);
867                 gtk_editable_set_position(GTK_EDITABLE(c->location), -1);
868                 return TRUE;
869             } else if (def_key("print", GDK_KEY_Print) == key) {
870                 WebKitPrintOperation *operation =
871                     webkit_print_operation_new(WEBKIT_WEB_VIEW(c->web_view));
872                 GtkWidget *toplevel = gtk_widget_get_toplevel(mw.win);
873                 webkit_print_operation_run_dialog(operation,
874                                                   GTK_WINDOW(toplevel));
875                 return TRUE;
876             } else if (def_key("quit", GDK_KEY_g) == key) {
877                 search(c, 2);
878                 gtk_widget_grab_focus(c->web_view);
879                 gtk_entry_set_text(GTK_ENTRY(c->location), uri);
880                 gtk_editable_set_position(GTK_EDITABLE(c->location), -1);
881                 webkit_web_view_run_javascript(
882                     WEBKIT_WEB_VIEW(c->web_view),
883                     "window.getSelection().removeAllRanges();"
884                     "document.activeElement.blur();",
885                     NULL, NULL, c);
886                 return TRUE;
887             } else if (def_key("reload", GDK_KEY_e) == key) {
888                 webkit_web_view_reload_bypass_cache(
889                     WEBKIT_WEB_VIEW(c->web_view));
890                 return TRUE;
891             } else if (def_key("scroll_line_down", GDK_KEY_j) == key) {
892                 for (int i = 0; i <= cfg.scroll_lines - 1; i++) {
893                     event->key.keyval = GDK_KEY_Down;
894                     gdk_event_put(event);
895                 }
896                 return TRUE;
897             } else if (def_key("scroll_line_up", GDK_KEY_k) == key) {
898                 for (int i = 0; i <= cfg.scroll_lines - 1; i++) {
899                     event->key.keyval = GDK_KEY_Up;
900                     gdk_event_put(event);
901                 }
902                 return TRUE;
903             } else if (def_key("scroll_page_down", GDK_KEY_f) == key) {
904                 event->key.keyval = GDK_KEY_Page_Down;
905                 gdk_event_put(event);
906                 return TRUE;
907             } else if (def_key("scroll_page_up", GDK_KEY_b) == key) {
908                 event->key.keyval = GDK_KEY_Page_Up;
909                 gdk_event_put(event);
910                 return TRUE;
911             } else if (def_key("search_forwards", GDK_KEY_s) == key) {
912                 search_init(c, 1);
913                 return TRUE;
914             } else if (def_key("search_backwards", GDK_KEY_r) == key) {
915                 search_init(c, -1);
916                 return TRUE;
917             } else if (def_key("tab_close", GDK_KEY_q) == key) {
918                 client_destroy(NULL, c);
919                 return TRUE;
920             } else if (def_key("tab_switch_1", GDK_KEY_1) == key) {
921                 gtk_notebook_set_current_page(GTK_NOTEBOOK(mw.notebook), 0);
922                 return TRUE;
923             } else if (def_key("tab_switch_2", GDK_KEY_2) == key) {
924                 gtk_notebook_set_current_page(GTK_NOTEBOOK(mw.notebook), 1);
925                 return TRUE;
926             } else if (def_key("tab_switch_3", GDK_KEY_3) == key) {
927                 gtk_notebook_set_current_page(GTK_NOTEBOOK(mw.notebook), 2);
928                 return TRUE;
929             } else if (def_key("tab_switch_4", GDK_KEY_4) == key) {
930                 gtk_notebook_set_current_page(GTK_NOTEBOOK(mw.notebook), 3);
931                 return TRUE;
932             } else if (def_key("tab_switch_5", GDK_KEY_5) == key) {
933                 gtk_notebook_set_current_page(GTK_NOTEBOOK(mw.notebook), 4);
934                 return TRUE;
935             } else if (def_key("tab_switch_6", GDK_KEY_6) == key) {
936                 gtk_notebook_set_current_page(GTK_NOTEBOOK(mw.notebook), 5);
937                 return TRUE;
938             } else if (def_key("tab_switch_7", GDK_KEY_7) == key) {
939                 gtk_notebook_set_current_page(GTK_NOTEBOOK(mw.notebook), 6);
940                 return TRUE;
941             } else if (def_key("tab_switch_8", GDK_KEY_8) == key) {
942                 gtk_notebook_set_current_page(GTK_NOTEBOOK(mw.notebook), 7);
943                 return TRUE;
944             } else if (def_key("tab_switch_9", GDK_KEY_9) == key) {
945                 gtk_notebook_set_current_page(GTK_NOTEBOOK(mw.notebook), 8);
946                 return TRUE;
947             } else if (def_key("tab_previous", GDK_KEY_u) == key) {
948                 gtk_notebook_prev_page(GTK_NOTEBOOK(mw.notebook));
949                 return TRUE;
950             } else if (def_key("tab_mute", GDK_KEY_m) == key) {
951                 gboolean muted =
952                     webkit_web_view_get_is_muted(WEBKIT_WEB_VIEW(c->web_view));
953                 webkit_web_view_set_is_muted(WEBKIT_WEB_VIEW(c->web_view),
954                                              !muted);
955                 changed_title(G_OBJECT(c->web_view), NULL, c);
956                 return TRUE;
957             } else if (def_key("tab_new", GDK_KEY_w) == key) {
958                 f = ensure_uri_scheme(cfg.home_uri);
959                 client_new(f, NULL, TRUE, TRUE);
960                 g_free(f);
961                 return TRUE;
962             } else if (def_key("tab_next", GDK_KEY_i) == key) {
963                 gtk_notebook_next_page(GTK_NOTEBOOK(mw.notebook));
964                 return TRUE;
965             } else if (def_key("toggle_js", GDK_KEY_o) == key) {
966                 gboolean on =
967                     webkit_settings_get_enable_javascript(c->settings);
968                 webkit_settings_set_enable_javascript(c->settings, !on);
969                 webkit_web_view_set_settings(WEBKIT_WEB_VIEW(c->web_view),
970                                              c->settings);
971                 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(c->jsbutton),
972                                              !on);
973             } else if (def_key("toggle_img", -1) == key) {
974                 gboolean on = webkit_settings_get_auto_load_images(c->settings);
975                 webkit_settings_set_auto_load_images(c->settings, !on);
976                 webkit_web_view_set_settings(WEBKIT_WEB_VIEW(c->web_view),
977                                              c->settings);
978                 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(c->imgbutton),
979                                              !on);
980             } else if (def_key("web_search", GDK_KEY_d) == key) {
981                 gtk_widget_grab_focus(c->location);
982                 gtk_entry_set_text(GTK_ENTRY(c->location), "w/");
983                 gtk_editable_set_position(GTK_EDITABLE(c->location), -1);
984                 return TRUE;
985             } else if (def_key("zoom_in", GDK_KEY_equal) == key) {
986                 now = webkit_web_view_get_zoom_level(
987                     WEBKIT_WEB_VIEW(c->web_view));
988                 webkit_web_view_set_zoom_level(WEBKIT_WEB_VIEW(c->web_view),
989                                                now + 0.1);
990                 return TRUE;
991             } else if (def_key("zoom_out", GDK_KEY_minus) == key) {
992                 now = webkit_web_view_get_zoom_level(
993                     WEBKIT_WEB_VIEW(c->web_view));
994                 webkit_web_view_set_zoom_level(WEBKIT_WEB_VIEW(c->web_view),
995                                                now - 0.1);
996                 return TRUE;
997             } else if (def_key("zoom_reset", GDK_KEY_0) == key) {
998                 webkit_web_view_set_zoom_level(WEBKIT_WEB_VIEW(c->web_view),
999                                                cfg.global_zoom);
1000                 return TRUE;
1001             }
1002         }
1003     }
1004     return FALSE;
1005 }
1006
1007 gboolean
1008 key_location(GtkWidget *widget, GdkEvent *event, gpointer data) {
1009     struct Client *c = (struct Client *)data;
1010     const gchar *t;
1011     gchar *f;
1012
1013     if (key_common(widget, event, data))
1014         return TRUE;
1015
1016     if (event->type == GDK_KEY_PRESS) {
1017         int key = ((GdkEventKey *)event)->keyval;
1018         if ((GDK_KEY_KP_Enter == key) || (GDK_KEY_Return == key)) {
1019             gtk_widget_grab_focus(c->web_view);
1020             t = gtk_entry_get_text(GTK_ENTRY(c->location));
1021             if (t != NULL && t[0] == 's' && t[1] == '/') {
1022                 if (search_text != NULL)
1023                     g_free(search_text);
1024                 search_text = g_strdup(t + 2);
1025                 search(c, 0);
1026             } else if (t != NULL && t[0] == 'w' && t[1] == '/') {
1027                 int len = strlen(cfg.search_engine) + strlen(t) - 2;
1028                 gchar *f = malloc(len + 1);
1029                 snprintf(f, len + 1, "%s%s", cfg.search_engine, t + 2);
1030                 webkit_web_view_load_uri(WEBKIT_WEB_VIEW(c->web_view), f);
1031                 g_free(f);
1032             } else {
1033                 f = ensure_uri_scheme(t);
1034                 webkit_web_view_load_uri(WEBKIT_WEB_VIEW(c->web_view), f);
1035                 g_free(f);
1036             }
1037             return TRUE;
1038         } else if (GDK_KEY_Escape == key) {
1039             t = webkit_web_view_get_uri(WEBKIT_WEB_VIEW(c->web_view));
1040             gtk_entry_set_text(GTK_ENTRY(c->location), (t == NULL) ? "" : t);
1041             return TRUE;
1042         }
1043     }
1044     return FALSE;
1045 }
1046
1047 gboolean
1048 key_tablabel(GtkWidget *widget, GdkEvent *event, gpointer data) {
1049     GdkScrollDirection direction;
1050
1051     if (event->type == GDK_BUTTON_RELEASE) {
1052         switch (((GdkEventButton *)event)->button) {
1053         case 2:
1054             client_destroy(NULL, data);
1055             return TRUE;
1056         }
1057     } else if (event->type == GDK_SCROLL) {
1058         gdk_event_get_scroll_direction(event, &direction);
1059         switch (direction) {
1060         case GDK_SCROLL_UP:
1061             gtk_notebook_prev_page(GTK_NOTEBOOK(mw.notebook));
1062             break;
1063         case GDK_SCROLL_DOWN:
1064             gtk_notebook_next_page(GTK_NOTEBOOK(mw.notebook));
1065             break;
1066         default:
1067             break;
1068         }
1069         return TRUE;
1070     }
1071     return FALSE;
1072 }
1073
1074 gboolean
1075 key_web_view(GtkWidget *widget, GdkEvent *event, gpointer data) {
1076     struct Client *c = (struct Client *)data;
1077     gdouble dx, dy;
1078     gfloat z;
1079
1080     if (key_common(widget, event, data))
1081         return TRUE;
1082
1083     if (event->type == GDK_KEY_PRESS) {
1084         if (((GdkEventKey *)event)->keyval == GDK_KEY_Escape) {
1085             webkit_web_view_stop_loading(WEBKIT_WEB_VIEW(c->web_view));
1086             gtk_entry_set_progress_fraction(GTK_ENTRY(c->location), 0);
1087         }
1088     } else if (event->type == GDK_BUTTON_RELEASE) {
1089         switch (((GdkEventButton *)event)->button) {
1090         case 2:
1091             if (c->hover_uri != NULL) {
1092                 client_new(c->hover_uri, NULL, TRUE, FALSE);
1093                 return TRUE;
1094             }
1095             break;
1096         case 8:
1097             webkit_web_view_go_back(WEBKIT_WEB_VIEW(c->web_view));
1098             return TRUE;
1099         case 9:
1100             webkit_web_view_go_forward(WEBKIT_WEB_VIEW(c->web_view));
1101             return TRUE;
1102         }
1103     } else if (event->type == GDK_SCROLL) {
1104         event->scroll.delta_y *= cfg.scroll_lines;
1105         if (((GdkEventScroll *)event)->state & GDK_CONTROL_MASK) {
1106             gdk_event_get_scroll_deltas(event, &dx, &dy);
1107             z = webkit_web_view_get_zoom_level(WEBKIT_WEB_VIEW(c->web_view));
1108             z += -dy * 0.1;
1109             z = dx != 0 ? cfg.global_zoom : z;
1110             webkit_web_view_set_zoom_level(WEBKIT_WEB_VIEW(c->web_view), z);
1111             return TRUE;
1112         }
1113     }
1114
1115     return FALSE;
1116 }
1117
1118 void
1119 mainwindow_setup(void) {
1120     mw.win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
1121     gtk_window_set_default_size(GTK_WINDOW(mw.win), 800, 600);
1122     g_signal_connect(G_OBJECT(mw.win), "destroy", gtk_main_quit, NULL);
1123
1124     gchar *priv = (cfg.private) ? "-private" : "";
1125     gchar *title = malloc(strlen(priv) + strlen(__NAME__) + 1);
1126     sprintf(title, "%s%s", __NAME__, priv);
1127     gtk_window_set_title(GTK_WINDOW(mw.win), title);
1128     g_free(title);
1129
1130     mw.notebook = gtk_notebook_new();
1131     gtk_notebook_set_scrollable(GTK_NOTEBOOK(mw.notebook), TRUE);
1132     gtk_container_add(GTK_CONTAINER(mw.win), mw.notebook);
1133     g_signal_connect(G_OBJECT(mw.notebook), "switch-page",
1134                      G_CALLBACK(notebook_switch_page), NULL);
1135 }
1136
1137 void
1138 mainwindow_title(gint idx) {
1139     GtkWidget *child, *widg, *tablabel;
1140     const gchar *text;
1141
1142     child = gtk_notebook_get_nth_page(GTK_NOTEBOOK(mw.notebook), idx);
1143     if (child == NULL)
1144         return;
1145
1146     widg = gtk_notebook_get_tab_label(GTK_NOTEBOOK(mw.notebook), child);
1147     tablabel =
1148         (GtkWidget *)g_object_get_data(G_OBJECT(widg), "chorizo-tab-label");
1149     text = gtk_label_get_text(GTK_LABEL(tablabel));
1150     gtk_window_set_title(GTK_WINDOW(mw.win), text);
1151 }
1152
1153 void
1154 notebook_switch_page(GtkNotebook *nb, GtkWidget *p, guint idx, gpointer data) {
1155     mainwindow_title(idx);
1156 }
1157
1158 gboolean
1159 quit_if_nothing_active(void) {
1160     if (clients == 0) {
1161         if (downloads == 0) {
1162             gtk_main_quit();
1163             return TRUE;
1164         } else {
1165             downloadmanager_show();
1166         }
1167     }
1168
1169     return FALSE;
1170 }
1171
1172 gboolean
1173 remote_msg(GIOChannel *channel, GIOCondition condition, gpointer data) {
1174     gchar *uri = NULL;
1175
1176     g_io_channel_read_line(channel, &uri, NULL, NULL, NULL);
1177     if (uri) {
1178         g_strstrip(uri);
1179         client_new(uri, NULL, TRUE, TRUE);
1180         g_free(uri);
1181     }
1182     return TRUE;
1183 }
1184
1185 void
1186 show_web_view(WebKitWebView *web_view, gpointer data) {
1187     struct Client *c = (struct Client *)data;
1188     gint idx;
1189
1190     (void)web_view;
1191
1192     gtk_widget_show_all(mw.win);
1193
1194     if (c->focus_new_tab) {
1195         idx = gtk_notebook_page_num(GTK_NOTEBOOK(mw.notebook), c->vbox);
1196         if (idx != -1)
1197             gtk_notebook_set_current_page(GTK_NOTEBOOK(mw.notebook), idx);
1198
1199         gtk_widget_grab_focus(c->web_view);
1200     }
1201 }
1202
1203 void
1204 trust_user_certs(WebKitWebContext *wc) {
1205     GTlsCertificate *cert;
1206     gchar *basedir, *absfile;
1207     const gchar *file;
1208     GDir *dir = NULL;
1209
1210     basedir = g_build_filename(g_get_user_data_dir(), __NAME__, "certs", NULL);
1211     dir = g_dir_open(basedir, 0, NULL);
1212     g_free(basedir);
1213     if (dir != NULL) {
1214         file = g_dir_read_name(dir);
1215         while (file != NULL) {
1216             absfile = g_build_filename(g_get_user_data_dir(), __NAME__, "certs",
1217                                        file, NULL);
1218             cert = g_tls_certificate_new_from_file(absfile, NULL);
1219             g_free(absfile);
1220             if (cert == NULL)
1221                 fprintf(stderr, __NAME__ ": Could not load trusted cert '%s'\n",
1222                         file);
1223             else
1224                 webkit_web_context_allow_tls_certificate_for_host(wc, cert,
1225                                                                   file);
1226             file = g_dir_read_name(dir);
1227         }
1228         g_dir_close(dir);
1229     }
1230 }
1231
1232 GKeyFile *
1233 get_ini(void) {
1234     GKeyFileFlags flags = G_KEY_FILE_NONE;
1235     config = g_key_file_new();
1236
1237     // Load user config
1238     gchar *fname = g_build_filename(g_get_user_config_dir(), __NAME__,
1239                                     "chorizo.ini", NULL);
1240     if (!g_key_file_load_from_file(config, fname, flags, NULL)) {
1241         // Load global config
1242         if (!g_key_file_load_from_file(config, "/etc/chorizo.ini", flags,
1243                                        NULL)) {
1244             fprintf(stderr, "Could not load chorizo.ini");
1245         }
1246     }
1247     g_free(fname);
1248     return config;
1249 }
1250
1251 int
1252 main(int argc, char **argv) {
1253     int opt, i;
1254
1255     while ((opt = getopt(argc, argv, "Cpv")) != -1) {
1256         switch (opt) {
1257         case 'C':
1258             cfg.cooperative_instances = FALSE;
1259             break;
1260         case 'p':
1261             cfg.private = true;
1262             break;
1263         case 'v':
1264             printf("%s %s\n", __NAME__, VERSION);
1265             exit(0);
1266         default:
1267             fprintf(stderr, "Usage: " __NAME__ " [OPTION]... [URI]...\n");
1268             exit(EXIT_FAILURE);
1269         }
1270     }
1271
1272     gtk_init(&argc, &argv);
1273
1274     // Keep clipboard contents after program closes
1275     gtk_clipboard_store(gtk_clipboard_get_for_display(gdk_display_get_default(),
1276                                                       GDK_SELECTION_CLIPBOARD));
1277
1278     get_config();
1279
1280     if (cfg.cooperative_instances)
1281         cooperation_setup();
1282
1283     if (!cfg.cooperative_instances || cfg.cooperative_alone)
1284         init_default_web_context();
1285
1286     downloadmanager_setup();
1287     mainwindow_setup();
1288
1289     client_arr = malloc(sizeof(struct Client *));
1290     if (optind >= argc) {
1291         client_new(cfg.home_uri, NULL, TRUE, TRUE);
1292     } else {
1293         for (i = optind; i < argc; i++)
1294             client_new(argv[i], NULL, TRUE, TRUE);
1295     }
1296
1297     if (!cfg.cooperative_instances || cfg.cooperative_alone)
1298         gtk_main();
1299
1300     for (int i = 0; i < clients; i++) {
1301         free(&(client_arr[i]));
1302     }
1303
1304     exit(EXIT_SUCCESS);
1305 }