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