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