adji

Adji's a Decisive and Joyful Internet browser
git clone https://noxz.tech/git/adji.git
Log | Files | Tags | LICENSE

browser.c
1/**
2 * Copyright (C) 2023 Chris Noxz
3 * Author(s): Chris Noxz <chris@noxz.tech>
4 *
5 * This program is free software: you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License as published by the Free
7 * Software Foundation, either version 3 of the License, or (at your option)
8 * any later version.
9 *
10 * This program is distributed in the hope that it will be useful, but WITHOUT
11 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13 * more details.
14 *
15 * You should have received a copy of the GNU General Public License along with
16 * this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18
19#include "browser.h"
20#include "config.h"
21
22
23void
24about_scheme_request(
25	WebKitURISchemeRequest                     *request,
26	gpointer                                    data)
27{
28	(void) data;
29
30	GError                 *e;                  /* error holder */
31	GInputStream           *s;                  /* stream to send */
32	GString                *m;                  /* message of stream */
33	const gchar            *p;                  /* path holder eg. 'config' */
34	gchar                 **w;                  /* temporary word holder */
35	gsize                   sl;                 /* size of stream */
36
37	p = request_path(request);
38
39	/* about:config */
40	if (!strcmp(p, "config")) {
41		g_string_append_printf((m = g_string_new(NULL)), "<html><body>"
42		    "<table style=\"width:100%%;\">"
43		    "<tr>"
44		    "<th align=\"left\">Nonconfigurable Name</th>"
45		    "<th align=\"left\">Value</th>"
46		    "</tr>"
47		    "<tr><td>"__NAME__" version</td><td>%s</td></tr>"
48		    "<tr><td>Gtk version</td><td>%u.%u.%u</td></tr>"
49		    "<tr><td>WebKit version</td><td>%u.%u.%u</td></tr>"
50		    "<tr>"
51		    "<th align=\"left\">Configurable Name</th>"
52		    "<th align=\"left\">Value</th>"
53		    "</tr>",
54		    VERSION,
55		    gtk_get_major_version(),
56		    gtk_get_minor_version(),
57		    gtk_get_micro_version(),
58		    webkit_get_major_version(),
59		    webkit_get_minor_version(),
60		    webkit_get_micro_version());
61		for (int i = 0; i < CFN_LAST; i++) {
62			if (!cfg[i].e)
63				continue;
64			g_string_append_printf(m, "<tr><td>%s</td><td>", cfg[i].e);
65			switch (cfg[i].t) {
66			case CFG_STRING:
67				g_string_append_printf(m, "%s", cfg[i].v.s);
68				break;
69			case CFG_INT:
70				g_string_append_printf(m, "%d", cfg[i].v.i);
71				break;
72			case CFG_FLOAT:
73				g_string_append_printf(m, "%f", cfg[i].v.f);
74				break;
75			case CFG_BOOL:
76				g_string_append_printf(m, "%s", cfg[i].v.b ? "TRUE" : "FALSE");
77				break;
78			case CFG_LIST:
79				for (w = cfg[i].v.l; w && *w; w++)
80					g_string_append_printf(m, "%s%s", w == cfg[i].v.l
81					                       ? "" : "; ", *w);
82				break;
83			}
84			g_string_append_printf(m, "</td></tr>");
85		}
86		g_string_append_printf(m, "</table>");
87		g_string_append_printf(m, "</body></html>");
88	} else {
89		webkit_uri_scheme_request_finish_error(request, (e = g_error_new(
90		    BROWSER_ERROR,
91		    BER_INVALID_ABOUT_PATH,
92		    "Invalid 'about:%s' page", p)));
93		g_error_free(e);
94		return;
95	}
96
97	sl = strlen(m->str);
98	s = g_memory_input_stream_new_from_data(m->str, sl, NULL);
99	webkit_uri_scheme_request_finish(request, s, sl, "text/html");
100	g_object_unref(s);
101	g_string_free(m, TRUE);
102}
103
104void
105attach_to_host(
106	GtkWidget                                  *self)
107{
108	guchar                 *d = NULL;           /* data holder */
109	gint                    l,                  /* data length */
110	                        f;                  /* data format */
111	GdkAtom                 t;                  /* data atom type */
112	GtkWidget               *w;                 /* top widget (window?) */
113
114	if (GTK_IS_PLUG(w = gtk_widget_get_toplevel(self)) || !GTK_IS_WINDOW(w))
115		return;
116
117	if (gdk_property_get(get_widget_win(GTK_WIDGET(w)),
118	                     gdk_atom_intern(CFG_S(CFN_TAB_HOST), FALSE), GDK_NONE,
119	                     0, G_MAXLONG, FALSE, &t, &f, &l, &d)
120	                     && t == gdk_atom_intern("WINDOW", FALSE)
121	                     && f == 32
122	                     && l == sizeof(Window))
123		gl.embed = (long)*((Window*)d);
124	else
125		gl.embed = 0;
126	g_free(d);
127}
128
129void
130attach_to_window(
131	GtkWidget                                  *self)
132{
133	GtkWidget *vbx;
134	GtkWidget *wv;
135
136	gl.embed = 0;
137
138	if (!gl.initialized)
139		return;
140
141	gl.win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
142
143	if (!(vbx = gtk_bin_get_child(GTK_BIN(self))))
144		return;
145
146	if (!(wv = gtk_container_get_children(GTK_CONTAINER(vbx))->data))
147		return;
148
149	/* window settings */
150	gtk_window_set_default_size(GTK_WINDOW(gl.win), 800, 600);
151	gtk_window_set_title(GTK_WINDOW(gl.win), __NAME__);
152
153	/* connect signal callbacks */
154	CB(gl.win,              "destroy",          cb_quit);
155	CB(gl.win,              "map-event",        cb_map);
156
157	g_signal_handlers_disconnect_by_data(G_OBJECT(self), NULL);
158
159	g_object_ref(vbx);
160	gtk_container_remove(GTK_CONTAINER(self), vbx);
161	gtk_container_add(GTK_CONTAINER(gl.win), vbx);
162	g_object_unref(vbx);
163
164	gtk_widget_destroy(self);
165	gtk_widget_show_all(gl.win);
166	gtk_window_set_focus(GTK_WINDOW(gl.win), wv);
167}
168
169void
170fork_url(
171	const gchar                                *uri)
172{
173	char e[64];
174
175	if (gl.embed > 0 && snprintf(e, sizeof(e), "%lu", gl.embed) > 0)
176		execvp(gl.arg0, (char *[]){ gl.arg0, "-E", e, (char*)uri, NULL });
177	else
178		execvp(gl.arg0, (char *[]){ gl.arg0, (char*)uri, NULL });
179}
180
181void
182initialize(
183	const gchar                                *uri)
184{
185	gchar                  *u = NULL;           /* translated uri */
186	int                     i;
187
188	/* communicate uri through ipc if constructed */
189	if (gl.initialized && uri) {
190		if ((u = resolve_uri(uri)) && fork() == 0)
191			fork_url(u);
192		g_free(u);
193		return; /* the request was handled elsewhere */
194	}
195
196	if (gl.initialized)
197		return;
198
199	/* do not create a new client if existing uri was resolved to NULL, as it
200	 * was handled as an XDG-OPEN event. Still process uri == NULL for "create"
201	 * callback signals, such as "Open Link in New Window". */
202	if (uri && !(u = resolve_uri(uri)))
203		return;
204
205	gl.wv = webkit_web_view_new();
206
207	/* connect signal callbacks */
208	CB(gl.wv, "button-release-event",           cb_wv_hid);
209	CB(gl.wv, "close",                          cb_wv_close);
210	CB(gl.wv, "context-menu",                   cb_context_menu);
211	CB(gl.wv, "create",                         cb_wv_create);
212	CB(gl.wv, "decide-policy",                  cb_wv_decide_policy);
213	CB(gl.wv, "key-press-event",                cb_wv_hid);
214	CB(gl.wv, "load-changed",                   cb_wv_load_changed);
215	CB(gl.wv, "load-failed-with-tls-errors",    cb_wv_tls_load_failed);
216	CB(gl.wv, "mouse-target-changed",           cb_wv_hover);
217	CB(gl.wv, "notify::estimated-load-progress",cb_wv_load_progress_changed);
218	CB(gl.wv, "notify::favicon",                cb_favicon_changed);
219	CB(gl.wv, "notify::title",                  cb_title_changed);
220	CB(gl.wv, "notify::uri",                    cb_uri_changed);
221	CB(gl.wv, "scroll-event",                   cb_wv_hid);
222	CB(gl.wv, "web-process-crashed",            cb_wv_crashed);
223	CB(gl.wv, "enter-fullscreen",               cb_wv_fullscreen_enter);
224	CB(gl.wv, "leave-fullscreen",               cb_wv_fullscreen_leave);
225
226	/* load config into webkit settings */
227	gl.settings = webkit_settings_new();
228	for (i = 0; i < CFN_LAST; i++)
229		if (cfg[i].s)
230			g_object_set(
231			    G_OBJECT(gl.settings),
232			    cfg[i].s,
233			    cfg[i].i ? (Arg)!(cfg[i].v.b) : (cfg[i].v),
234			    NULL
235			);
236	webkit_web_view_set_settings(WEBKIT_WEB_VIEW(gl.wv), gl.settings);
237
238	if (CFG_L(CFN_ACCEPTED_LANGUAGES))
239		webkit_web_context_set_preferred_languages(
240		    webkit_web_view_get_context(WEBKIT_WEB_VIEW(gl.wv)),
241		    CFG_L(CFN_ACCEPTED_LANGUAGES)
242		);
243	if (CFG_S(CFN_COOKIE_FILE))
244		webkit_cookie_manager_set_persistent_storage(
245		    webkit_web_context_get_cookie_manager(
246		        webkit_web_view_get_context(WEBKIT_WEB_VIEW(gl.wv))
247		    ),
248		    CFG_S(CFN_COOKIE_FILE),
249		    WEBKIT_COOKIE_PERSISTENT_STORAGE_TEXT
250		);
251	if (CFG_S(CFN_PROXY_URI))
252		webkit_website_data_manager_set_network_proxy_settings(
253		    webkit_web_view_get_website_data_manager(WEBKIT_WEB_VIEW(gl.wv)),
254		    WEBKIT_NETWORK_PROXY_MODE_CUSTOM,
255		    webkit_network_proxy_settings_new(
256		        CFG_S(CFN_PROXY_URI), CFG_L(CFN_PROXY_IGNORE)
257		    )
258		);
259	webkit_web_view_set_zoom_level(
260	    WEBKIT_WEB_VIEW(gl.wv), CFG_F(CFN_ZOOM_LEVEL)
261	);
262
263	/* create entry */
264	gl.entry = gtk_entry_new();
265	CB(gl.entry, "key-press-event",             cb_entry_hid);
266	CB(gl.entry, "icon-release",                cb_entry_icon_hid);
267	gtk_entry_set_icon_from_icon_name(
268	    GTK_ENTRY(gl.entry), GTK_ENTRY_ICON_SECONDARY,
269	    CFG_B(CFN_DISABLE_JAVASCRIPT) ? ICON_JS_OFF : ICON_JS_ON
270	);
271
272	/* create vertical box to store the web view and the entry */
273	gl.vbx = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
274	gtk_box_pack_start(GTK_BOX(gl.vbx), gl.wv, TRUE, TRUE, 0);
275	gtk_box_pack_start(GTK_BOX(gl.vbx), gl.entry, FALSE, FALSE, 0);
276	gtk_container_set_focus_child(GTK_CONTAINER(gl.vbx), gl.wv);
277
278	/* pack everything in a vertical box */
279	gtk_container_add(GTK_CONTAINER(gl.win), gl.vbx);
280
281	/* manage which tab should be focused */
282	gtk_widget_show_all(gl.win);
283	gtk_window_set_focus(GTK_WINDOW(gl.win), gl.wv);
284
285	/* finally load the uri */
286	if (u)
287		webkit_web_view_load_uri(WEBKIT_WEB_VIEW(gl.wv), u);
288	g_free(u);
289
290	load_user_styles(WEBKIT_WEB_VIEW(gl.wv));
291	load_user_scripts(WEBKIT_WEB_VIEW(gl.wv));
292
293	if (uri && !strcmp(CFG_S(CFN_HOME_URI), uri))
294		gtk_widget_grab_focus(gl.entry);
295
296	gl.initialized = TRUE;
297}
298
299void
300terminate(void)
301{
302	/* disconnect all handlers */
303	g_signal_handlers_disconnect_by_data(G_OBJECT(gl.wv), NULL);
304
305	gl.initialized = FALSE;
306	quit();
307}
308
309gboolean
310command(
311	const gchar                                *t)
312{
313	if (t[0] == '/') {          /* in-page search */
314		g_free(gl.search_text);
315		gl.search_text = g_strdup(t + 1);
316		search(IPS_INIT);
317		return TRUE;
318	} else if (t[0] == 'q') {   /* quit (vim-like) */
319		terminate();
320		return TRUE;
321	}
322
323	return FALSE;
324}
325
326void
327create_context_menu(
328	WebKitContextMenu                          *context_menu,
329	WebKitHitTestResult                        *hit_test_result)
330{
331	guint                   x;                  /* hit test result context */
332
333	x = webkit_hit_test_result_get_context(hit_test_result);
334
335	webkit_context_menu_prepend(
336	    context_menu, webkit_context_menu_item_new_separator()
337	);
338
339	/* if document is the only context */
340	if (x == WEBKIT_HIT_TEST_RESULT_CONTEXT_DOCUMENT)
341		return create_context_menu_item(
342		    context_menu,
343		    "open-external",
344		    "Open Page Externally",
345		    cb_open_external
346		); /* no need to check further */
347
348	if (x & WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK)
349		create_context_menu_item(
350		    context_menu,
351		    "open-external",
352		    "Open Link Externally",
353		    cb_open_external
354		);
355
356	/* requires javascript for DOM access from here on */
357	if (!webkit_settings_get_enable_javascript(gl.settings))
358		return;
359
360	if (x & WEBKIT_HIT_TEST_RESULT_CONTEXT_SELECTION)
361		create_context_menu_item(
362		    context_menu,
363		    "selection-search",
364		    "Search Selection",
365		    cb_selection_search
366		);
367}
368
369void
370create_context_menu_item(
371	WebKitContextMenu                          *context_menu,
372	const gchar                                *name,
373	const gchar                                *label,
374	void                                       *action)
375{
376	GAction                *a = (GAction*)g_simple_action_new(name, NULL);
377
378	CB(a, "activate", action);
379	webkit_context_menu_prepend(
380	    context_menu,
381	    webkit_context_menu_item_new_from_gaction(a, label, NULL)
382	);
383	g_object_unref(a);
384}
385
386void
387download_blob(
388	WebKitDownload                             *d)
389{
390	WebKitURIResponse *r = webkit_download_get_response(d);
391	const gchar *u = webkit_uri_response_get_uri(r);
392
393	if (strncmp(u, "blob:", 5) && strncmp(u, "data:", 5))
394		return;
395
396	webkit_download_set_destination(d, CFG_S(CFN_BLOB_FILE));
397}
398
399void
400download_response(
401	WebKitURIResponse                          *r)
402{
403	xdg_open("adjidl:", webkit_uri_response_get_uri(r), TRUE);
404}
405
406gchar*
407get_uri(
408	GtkWidget                                  *wv)
409{
410	const gchar            *u;
411
412	if (!(u = webkit_web_view_get_uri(WEBKIT_WEB_VIEW(wv))))
413		return NULL;
414
415	if (!strncmp(u, ABOUT_SCHEME":", strlen(ABOUT_SCHEME":")))
416		return g_strconcat("about:", u + strlen(ABOUT_SCHEME":"), NULL);
417
418	return g_strdup(u);
419}
420
421gboolean
422handle_fullscreen(
423	gboolean                                    fullscreen)
424{
425	GdkDisplay             *d = gdk_display_get_default();
426	GdkWindow              *p = GTK_IS_PLUG(gl.win)
427	                          ? get_socket_win(gl.win)
428	                          : to_gdk_win(d, gl.embed);
429
430	if (fullscreen) /* hide or show widgets depending on fullscreen */
431		gtk_widget_hide(gl.entry);
432	else
433		gtk_widget_show_all(gl.entry);
434
435	if (!p) /* if not embeded we are done! */
436		return TRUE;
437
438	gtk_widget_grab_focus(gl.wv);
439
440	if (fullscreen) /* tell parent to go fullscreen */
441		gdk_window_fullscreen(p);
442	else
443		gdk_window_unfullscreen(p);
444	return TRUE;
445}
446
447void
448handle_popup(
449	const gchar                                *uri)
450{
451	char u[8192];
452	snprintf(u, sizeof(u), "%lu:%s", gl.embed, uri);
453
454	switch (CFG_I(CFN_POPUP_POLICY)) {
455	case PUP_ALLOW:
456		initialize(uri);
457		break;
458	case PUP_EXTERNAL:
459		xdg_open("adjipu:", u, TRUE);
460		break;
461	}
462}
463
464gboolean
465key_common(
466	GdkEventKey                                *event)
467{
468	gchar                  *u = NULL;
469	gboolean                f = is_fullscreen();
470
471	/* key presses not using the alt key */
472	switch (event->keyval) {
473	case GDK_KEY_F11: /* toggle window fullscreen state */
474		if (f) {
475			gtk_window_unfullscreen(GTK_WINDOW(gl.win));
476			handle_fullscreen(!f);
477		} else {
478			gtk_window_fullscreen(GTK_WINDOW(gl.win));
479			handle_fullscreen(f);
480		}
481		return TRUE;
482	}
483
484	if (event->state & GDK_CONTROL_MASK) /* ctrl key commands */
485		switch (event->keyval) {
486		case GDK_KEY_h:
487		case GDK_KEY_j:
488		case GDK_KEY_k:
489		case GDK_KEY_l: /* in-page movement by emulating arrow keys... */
490		case GDK_KEY_g:
491		case GDK_KEY_G: /* ... as well as home and end */
492			event->keyval /* translate key press accordingly */
493			    = event->keyval == GDK_KEY_h ? GDK_KEY_Left
494			    : event->keyval == GDK_KEY_j ? GDK_KEY_Down
495			    : event->keyval == GDK_KEY_k ? GDK_KEY_Up
496			    : event->keyval == GDK_KEY_l ? GDK_KEY_Right
497			    : event->keyval == GDK_KEY_g ? GDK_KEY_Home
498			    : /*                        */ GDK_KEY_End;
499			event->state = 0;
500			gtk_propagate_event(GTK_WIDGET(gl.wv), (GdkEvent*)event);
501			return TRUE;
502		}
503
504	if (f) /* do not handle key events in fullscreen */
505		return FALSE;
506
507	/* only handle key presses using the alt key */
508	if (!(event->state & GDK_MOD1_MASK))
509		return FALSE;
510
511	switch (event->keyval) {
512	case GDK_KEY_q: /* destroy client and close tab */
513		terminate();
514		return TRUE;
515	case GDK_KEY_w: /* go to home page */
516		if ((u = resolve_uri(CFG_S(CFN_HOME_URI))))
517			webkit_web_view_load_uri(WEBKIT_WEB_VIEW(gl.wv), u);
518		g_free(u);
519		return TRUE;
520	case GDK_KEY_r: /* reload, while bypassing cache */
521		webkit_web_view_reload_bypass_cache(WEBKIT_WEB_VIEW(gl.wv));
522		return TRUE;
523	case GDK_KEY_i:
524		toggle_inspector();
525		return TRUE;
526	case GDK_KEY_o: /* focus entry */
527		gtk_widget_grab_focus(gl.entry);
528		return TRUE;
529	case GDK_KEY_n: /* search forward */
530		search(IPS_FORWARD);
531		return TRUE;
532	case GDK_KEY_N: /* search backward */
533		search(IPS_BACKWARD);
534		return TRUE;
535	case GDK_KEY_slash: /* initiate in-page search */
536		gtk_widget_grab_focus(gl.entry);
537		gtk_entry_set_text(GTK_ENTRY(gl.entry), ":/");
538		gtk_editable_set_position(GTK_EDITABLE(gl.entry), -1);
539		return TRUE;
540	case GDK_KEY_0:
541	case GDK_KEY_minus:
542	case GDK_KEY_equal: /* set zoom level */
543		webkit_web_view_set_zoom_level(
544		    WEBKIT_WEB_VIEW(gl.wv),
545		    event->keyval == GDK_KEY_0
546		    ? CFG_F(CFN_ZOOM_LEVEL)
547		    : webkit_web_view_get_zoom_level(WEBKIT_WEB_VIEW(gl.wv))
548		    + ((event->keyval == GDK_KEY_minus) ? -0.1 : 0.1)
549		);
550		return TRUE;
551	case GDK_KEY_H: /* go back in history */
552		webkit_web_view_go_back(WEBKIT_WEB_VIEW(gl.wv));
553		return TRUE;
554	case GDK_KEY_L: /* go forward in history */
555		webkit_web_view_go_forward(WEBKIT_WEB_VIEW(gl.wv));
556		return TRUE;
557	case GDK_KEY_s: /* toggle javascript (on or off) */
558		set_javascript_policy(JSP_TOGGLE);
559		return TRUE;
560	case GDK_KEY_c: /* toggle tls error policy (ignore or fail) */
561		toggle_tls_error_policy();
562		return TRUE;
563	case GDK_KEY_p: /* open print dialog */
564		webkit_print_operation_run_dialog(
565		    webkit_print_operation_new(WEBKIT_WEB_VIEW(gl.wv)),
566		    GTK_WINDOW(gl.win)
567		);
568		return TRUE;
569	}
570
571	/* check external handler keys */
572	if (cfg[CFN_EXTERNAL_HANDLER_KEYS].v.b &&
573	    event->keyval < 256 &&
574	    g_strv_contains(
575	        CFG_L(CFN_EXTERNAL_HANDLER_KEYS), (gchar[]){(gchar)event->keyval, 0}
576	    )) {
577		open_external((gchar)event->keyval);
578		return TRUE;
579	}
580
581	return FALSE;
582}
583
584gboolean
585key_entry(
586	GdkEventKey                                *event)
587{
588	const gchar            *t;
589	gchar                  *u = NULL,
590	                       *l = NULL;
591	int                     p,
592	                        m;
593
594	/* handle key presses using the alt key (takes precedence over common) */
595	if (event->state & GDK_MOD1_MASK) {
596		switch (event->keyval) { /* movements inside entry */
597		case GDK_KEY_l:
598		case GDK_KEY_h:
599			p = gtk_editable_get_position(GTK_EDITABLE(gl.entry));
600			m = gtk_entry_get_text_length(GTK_ENTRY(gl.entry));
601			p += event->keyval == GDK_KEY_l
602				? m > p ? 1 : 0     /* move right */
603				: 0 < p ? -1 : 0;   /* move left */
604			gtk_editable_set_position(GTK_EDITABLE(gl.entry), p);
605			return TRUE;
606		}
607	}
608
609	/* handle common key presses */
610	if (key_common(event))
611		return TRUE;
612
613	/* handle any key press (not just using the alt key) */
614	switch (event->keyval) {
615	case GDK_KEY_KP_Enter:
616	case GDK_KEY_Return:
617		gtk_widget_grab_focus(gl.wv);
618		if (!(t = gtk_entry_get_text(GTK_ENTRY(gl.entry))))
619			return TRUE;
620		if (t[0] != ':') { /* if not a command */
621			/* store current uri before loading new uri */
622			l = get_uri(gl.wv);
623			if ((u = resolve_uri(t)))
624				webkit_web_view_load_uri(WEBKIT_WEB_VIEW(gl.wv), u);
625			g_free(u);
626			/* fix: notify::uri won't be raised if current uri is unchanged */
627			if (!strcmp(l, (u = get_uri(gl.wv))))
628				uri_changed();
629			g_free_all((gpointer[]){u, l, NULL});
630			return TRUE;
631		} else if (command(t + 1)) { /* if command */
632			return TRUE;
633		} /* FALL THROUGH */
634	case GDK_KEY_Escape:
635		gtk_widget_grab_focus(gl.wv);
636		u = get_uri(gl.wv);
637		gtk_entry_set_text(GTK_ENTRY(gl.entry), (!u ? __NAME__ : u));
638		gtk_editable_set_position(GTK_EDITABLE(gl.entry), -1);
639		g_free(u);
640		return TRUE;
641	}
642
643	return FALSE;
644}
645
646gboolean
647key_web_view(
648	GdkEvent                                   *event)
649{
650	gdouble                 dx,                 /* scroll x-delta */
651	                        dy;                 /* scroll y-delta */
652
653	/* handle common key presses */
654	if (event->type == GDK_KEY_PRESS && key_common((GdkEventKey *)event))
655		return TRUE;
656
657	/* escape key: stop web rendering */
658	if (event->type == GDK_KEY_PRESS
659	    && ((GdkEventKey *)event)->keyval == GDK_KEY_Escape) {
660		webkit_web_view_stop_loading(WEBKIT_WEB_VIEW(gl.wv));
661		gtk_entry_set_progress_fraction(GTK_ENTRY(gl.entry), 0);
662	/* mouse button events */
663	} else if (event->type == GDK_BUTTON_RELEASE) {
664		switch (((GdkEventButton *)event)->button) {
665		case 2:  /* mouse middle button: create new client */
666			if (gl.hover_uri) {
667				initialize(gl.hover_uri);
668				return TRUE;
669			}
670			break;
671		case 8:  /* mouse button back: go back in history */
672			webkit_web_view_go_back(WEBKIT_WEB_VIEW(gl.wv));
673			return TRUE;
674		case 9:  /* mouse button forward: go forward in history */
675			webkit_web_view_go_forward(WEBKIT_WEB_VIEW(gl.wv));
676			return TRUE;
677		}
678	/* scroll using alt pressed: zoom in or out */
679	} else if (event->type == GDK_SCROLL
680	           && (((GdkEventScroll *)event)->state & GDK_MOD1_MASK)) {
681		gdk_event_get_scroll_deltas(event, &dx, &dy);
682		webkit_web_view_set_zoom_level(
683		    WEBKIT_WEB_VIEW(gl.wv),
684		    dx != 0
685		    ? CFG_F(CFN_ZOOM_LEVEL)
686		    : webkit_web_view_get_zoom_level(WEBKIT_WEB_VIEW(gl.wv)) + -dy * 0.1
687		);
688		return TRUE;
689	}
690
691	return FALSE;
692}
693
694void
695load_changed(
696	WebKitLoadEvent                             event_type)
697{
698	switch (event_type) {
699	/* when page load starts, reset everything */
700	case WEBKIT_LOAD_STARTED:
701		gl.https = FALSE;
702		if (gl.error_page)
703			gl.error_page = FALSE;
704		else
705			g_clear_object(&gl.failed_crt);
706		break;
707	/* when page load is redirected, continue as usual */
708	case WEBKIT_LOAD_REDIRECTED:
709		break;
710	/* when page load is committed, get https and tls state */
711	case WEBKIT_LOAD_COMMITTED:
712		gl.https = webkit_web_view_get_tls_info(
713		    WEBKIT_WEB_VIEW(gl.wv), &gl.crt, &gl.tls_error
714		);
715		break;
716	/* when page load is finished, run all user scripts */
717	case WEBKIT_LOAD_FINISHED:
718		load_user_styles(WEBKIT_WEB_VIEW(gl.wv));
719		load_user_scripts(WEBKIT_WEB_VIEW(gl.wv));
720		break;
721	}
722
723	update_title();
724}
725
726void
727load_configuration(void)
728{
729	const gchar            *e;                  /* environment variable name */
730	int                     i;
731
732	/* set global defaults */
733	gl.search_text          = NULL;             /* in page search phrase */
734	gl.state_lock           = TRUE;             /* prevents state save */
735	gl.initialized          = FALSE;            /* is client initialized? */
736
737	/* load default configuration */
738	for (i = 0; i < CFN_LAST; i++) {
739		if (cfg[i].e && (e = g_getenv(cfg[i].e))) {
740			switch (cfg[i].t) {
741			case CFG_INT:       cfg[i].v.i = atoi(e); break;
742			case CFG_FLOAT:     cfg[i].v.f = atof(e); break;
743			case CFG_BOOL:      cfg[i].v.b = TRUE; break;
744			case CFG_STRING:    cfg[i].v.s = g_strdup(e); break;
745			case CFG_LIST:      cfg[i].v.l = g_strsplit(e, ",", -1); break;
746			}
747		}
748	}
749}
750
751void
752load_stdin(void)
753{
754	int                     c;                  /* character holder */
755	GString                *s = g_string_new(NULL);
756
757	/* read until end of stream */
758	while ((c = getc(stdin)) != EOF)
759		g_string_append_c(s, (gchar)c);
760
761	/* load the html content, which won't survive a reload */
762	initialize(NULL);
763	webkit_web_view_load_html(WEBKIT_WEB_VIEW(gl.wv), s->str, NULL);
764	g_string_free(s, TRUE);
765}
766
767void
768load_user_scripts(
769	WebKitWebView                              *web_view)
770{
771	gchar                  *c = NULL,           /* file content */
772	                       *p = NULL,           /* path (file) */
773	                       *b = NULL;           /* base directory (scripts) */
774	const gchar            *e = NULL;           /* directory file entry */
775	GDir                   *s = NULL;           /* user script directory */
776
777	webkit_user_content_manager_remove_all_scripts(
778	    webkit_web_view_get_user_content_manager(web_view)
779	);
780
781	IUS(web_view, SCRIPT_ALL_FRAMES, WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES);
782	IUS(web_view, SCRIPT_MAIN_FRAME, WEBKIT_USER_CONTENT_INJECT_TOP_FRAME);
783
784	b = g_build_filename(CFG_DIR, __NAME__, CFG_S(CFN_USER_SCRIPT_DIR), NULL);
785	if (!(s = g_dir_open(b, 0, NULL)))
786		return g_free(b);
787
788	while ((e = g_dir_read_name(s))) {
789		if (g_str_has_suffix((p = g_build_filename(b, e, NULL)), ".js")
790		    && g_file_get_contents(p, &c, NULL, NULL)) {
791			IUS(web_view, c, WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES);
792			g_free(c);
793		}
794		g_free(p);
795	}
796
797	g_dir_close(s);
798	g_free(b);
799}
800
801void
802load_user_styles(
803	WebKitWebView                              *web_view)
804{
805	gchar                  *c = NULL,           /* file content */
806	                       *p = NULL,           /* path (file) */
807	                       *b = NULL;           /* base directory (scripts) */
808	const gchar            *e = NULL,           /* directory file entry */
809	                       *u = NULL;           /* uri holder */
810	GDir                   *s = NULL;           /* user style directory */
811	gchar                 **m = NULL;           /* match container */
812
813	b = g_build_filename(CFG_DIR, __NAME__, CFG_S(CFN_USER_CSS_DIR), NULL);
814	if (!(s = g_dir_open(b, 0, NULL)))
815		return g_free(b);
816
817	if (!(u = webkit_web_view_get_uri(WEBKIT_WEB_VIEW(web_view))))
818		return; /* end load if no uri exists */
819
820	webkit_user_content_manager_remove_all_style_sheets(
821	    webkit_web_view_get_user_content_manager(web_view)
822	);
823
824	while ((e = g_dir_read_name(s))) {
825		if (g_str_has_suffix((p = g_build_filename(b, e, NULL)), ".css")
826		    && g_file_get_contents(p, &c, NULL, NULL)) {
827			/* reject uri specific css that do not match */
828			m = g_regex_split_simple("/\\* URL -- (.*) \\*/", c, 0, 0);
829			if (!m[1] || (m[1] != NULL && !strncmp(m[1], u, strlen(m[1]))))
830				IUSS(web_view, c, WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES);
831			g_free(m);
832			g_free(c);
833		}
834		g_free(p);
835	}
836
837	g_dir_close(s);
838	g_free(b);
839}
840
841void
842main_window_setup(void)
843{
844	/* define window elements */
845	gl.win                  = gl.embed > 0
846	                        ? gtk_plug_new(gl.embed)
847	                        : gtk_window_new(GTK_WINDOW_TOPLEVEL);
848
849	/* window settings */
850	gtk_window_set_default_size(GTK_WINDOW(gl.win), 800, 600);
851	gtk_window_set_title(GTK_WINDOW(gl.win), __NAME__);
852
853	/* connect signal callbacks */
854	CB(gl.win,              "destroy",          cb_quit);
855
856	if (gl.embed)
857		CB(gl.win,          "unmap-event",      cb_unmap);
858	CB(gl.win,              "map-event",        cb_map);
859}
860
861void
862open_external(
863	const gchar                                 s)
864{
865	char                    b[URI_MAX];         /* command line buffer */
866	gchar                  *p = NULL;           /* external handler path */
867	p = g_build_filename(
868	    CFG_DIR, __NAME__, CFG_S(CFN_EXTERNAL_HANDLER_FILE), NULL
869	);
870	if (s)
871		sprintf(b, "%s %c:%s &", p, s, gtk_entry_get_text(GTK_ENTRY(gl.entry)));
872	else
873		sprintf(b, "%s %s &", p, gtk_entry_get_text(GTK_ENTRY(gl.entry)));
874	g_free(p);
875
876	system(b);
877}
878
879void
880quit(void)
881{
882	gtk_main_quit();
883}
884
885void
886render_tls_error(
887	gchar                                      *uri,
888	GTlsCertificate                            *crt,
889	GTlsCertificateFlags                        crt_flags)
890{
891	GString                *m = NULL;           /* message (error) */
892	gchar                  *h = NULL,           /* html code */
893	                       *s = NULL,           /* subject name */
894	                       *i = NULL,           /* issuer name */
895	                       *b = NULL,           /* not before date */
896	                       *a = NULL,           /* not after date */
897	                       *p = NULL;           /* pem block */
898	GDateTime              *bd = NULL,          /* not before date */
899	                       *ad = NULL;          /* not after date */
900
901	m                       = g_string_new(NULL);
902	gl.failed_crt           = g_object_ref(crt);
903	gl.tls_error            = crt_flags;
904	gl.error_page           = TRUE;
905
906	/* translate all flags to messages */
907	if (gl.tls_error & G_TLS_CERTIFICATE_UNKNOWN_CA)
908		g_string_append(m, MSG_TLS_CERTIFICATE_UNKNOWN_CA);
909	if (gl.tls_error & G_TLS_CERTIFICATE_BAD_IDENTITY)
910		g_string_append(m, MSG_TLS_CERTIFICATE_BAD_IDENTITY);
911	if (gl.tls_error & G_TLS_CERTIFICATE_NOT_ACTIVATED)
912		g_string_append(m, MSG_TLS_CERTIFICATE_NOT_ACTIVATED);
913	if (gl.tls_error & G_TLS_CERTIFICATE_EXPIRED)
914		g_string_append(m, MSG_TLS_CERTIFICATE_EXPIRED);
915	if (gl.tls_error & G_TLS_CERTIFICATE_REVOKED)
916		g_string_append(m, MSG_TLS_CERTIFICATE_REVOKED);
917	if (gl.tls_error & G_TLS_CERTIFICATE_INSECURE)
918		g_string_append(m, MSG_TLS_CERTIFICATE_INSECURE);
919	if (gl.tls_error & G_TLS_CERTIFICATE_GENERIC_ERROR)
920		g_string_append(m, MSG_TLS_CERTIFICATE_GENERIC_ERROR);
921
922	/* construct html code and load it */
923	g_object_get(crt, "subject-name", &s, NULL);
924	g_object_get(crt, "issuer-name", &i, NULL);
925	g_object_get(crt, "not-valid-before", &bd, NULL);
926	g_object_get(crt, "not-valid-after", &ad, NULL);
927	g_object_get(crt, "certificate-pem", &p, NULL);
928	b = g_date_time_format_iso8601(bd);
929	a = g_date_time_format_iso8601(ad);
930	h = g_strdup_printf(TLS_MSG_FORMAT, uri, m->str, s, i, b, a, p);
931	webkit_web_view_load_alternate_html(WEBKIT_WEB_VIEW(gl.wv), h, uri, NULL);
932
933	g_string_free(m, TRUE);
934	g_free_all((gpointer[]){h, s, i, b, a, p, NULL});
935	g_date_time_unref(ad);
936	g_date_time_unref(bd);
937}
938
939void
940save_history(
941	const gchar                                *t)
942{
943	FILE                   *fp;
944
945	if (!CFG_S(CFN_HISTORY_FILE))
946		return;
947
948	if (!strcmp(t, ABOUT_BLANK))
949		return;
950
951	if (!(fp = fopen(CFG_S(CFN_HISTORY_FILE), "a")))
952		return perror(__NAME__": Error opening history file");
953
954	fprintf(fp, "%s\n", t);
955	fclose(fp);
956}
957
958void
959search(
960	enum inpage_search_type                     type)
961{
962	if (!gl.search_text)
963		return;
964
965	switch (type) {
966	case IPS_INIT:
967		webkit_find_controller_search(
968		    webkit_web_view_get_find_controller(WEBKIT_WEB_VIEW(gl.wv)),
969		    gl.search_text,
970		    WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE
971		    | WEBKIT_FIND_OPTIONS_WRAP_AROUND,
972		    G_MAXUINT
973		);
974		break;
975	case IPS_FORWARD:
976		webkit_find_controller_search_next(
977		    webkit_web_view_get_find_controller(WEBKIT_WEB_VIEW(gl.wv))
978		);
979		break;
980	case IPS_BACKWARD:
981		webkit_find_controller_search_previous(
982		    webkit_web_view_get_find_controller(WEBKIT_WEB_VIEW(gl.wv))
983		);
984		break;
985	}
986}
987
988void
989selection_search(void)
990{
991	webkit_web_view_evaluate_javascript(
992	    WEBKIT_WEB_VIEW(gl.wv),
993	    gl.first_search ? SCRIPT_SEARCH : SCRIPT_SEARCH_EXTRA,
994	    -1,
995	    NULL,
996	    NULL,
997	    NULL,
998	    cb_selection_search_finished,
999	    NULL
1000	);
1001}
1002
1003void
1004selection_search_finished(
1005	GAsyncResult                               *result)
1006{
1007	JSCValue               *v = NULL;
1008	gchar                  *s = NULL,
1009	                       *u = NULL;
1010
1011	if (!(v = webkit_web_view_evaluate_javascript_finish(
1012	    WEBKIT_WEB_VIEW(gl.wv), result, NULL)))
1013		return;
1014
1015	if (jsc_value_is_string(v)
1016	    && strlen(s = jsc_value_to_string(v))
1017	    && !jsc_context_get_exception(jsc_value_get_context(v))
1018	    && (u = resolve_uri(s)))
1019		initialize(u);
1020	else if (gl.first_search) {
1021		gl.first_search = FALSE;
1022		selection_search();
1023	}
1024
1025	g_free_all((gpointer[]){u, s, NULL});
1026}
1027
1028void
1029set_default_web_context(void)
1030{
1031	gchar                  *p = NULL;           /* web extensions path */
1032	WebKitWebContext       *c;                  /* web context */
1033
1034	c = webkit_web_context_get_default();
1035
1036	p = g_build_filename(CFG_DIR, __NAME__, CFG_S(CFN_WEB_EXTENSION_DIR), NULL);
1037#if GTK_CHECK_VERSION(3, 98, 0) /* seems to be fixed in 3.98.0, we'll see */
1038	webkit_web_context_set_sandbox_enabled(c, TRUE);
1039	webkit_web_context_add_path_to_sandbox(c, p, TRUE);
1040#else
1041	webkit_web_context_set_web_extensions_directory(c, p);
1042#endif
1043
1044	CB(c, "download-started", cb_download_start);
1045	webkit_web_context_set_favicon_database_directory(c, NULL);
1046	webkit_web_context_register_uri_scheme(
1047	    c, ABOUT_SCHEME, (WebKitURISchemeRequestCallback)about_scheme_request,
1048	    NULL, NULL
1049	);
1050
1051	g_free(p);
1052}
1053
1054void
1055set_hover_uri(
1056	WebKitHitTestResult                        *hit_test_result)
1057{
1058	const char             *t;                  /* entry text holder */
1059	gchar                  *u = NULL;           /* uri text holder */
1060
1061	g_free(gl.hover_uri);
1062
1063	/* only display hovered links */
1064	if (webkit_hit_test_result_context_is_link(hit_test_result)) {
1065		t = webkit_hit_test_result_get_link_uri(hit_test_result);
1066		gl.hover_uri = g_strdup(t);
1067	} else {
1068		u = get_uri(gl.wv);
1069		gl.hover_uri = NULL;
1070	}
1071
1072	if (!gtk_widget_is_focus(gl.entry))
1073		gtk_entry_set_text(GTK_ENTRY(gl.entry), u ? u : t);
1074
1075	g_free(u);
1076}
1077
1078void
1079set_javascript_policy(
1080	enum javascript_policy                      policy)
1081{
1082	webkit_settings_set_enable_javascript_markup(
1083	    gl.settings,
1084	    policy == JSP_TOGGLE
1085	        ? !webkit_settings_get_enable_javascript_markup(gl.settings)
1086	        : policy
1087	);
1088	gtk_entry_set_icon_from_icon_name(
1089	    GTK_ENTRY(gl.entry),
1090	    GTK_ENTRY_ICON_SECONDARY,
1091	    webkit_settings_get_enable_javascript_markup(gl.settings)
1092	        ? ICON_JS_ON
1093	        : ICON_JS_OFF
1094	);
1095	webkit_web_view_reload_bypass_cache(WEBKIT_WEB_VIEW(gl.wv));
1096}
1097
1098void
1099toggle_inspector(void)
1100{
1101	WebKitWebInspector     *i;
1102
1103	i = webkit_web_view_get_inspector(WEBKIT_WEB_VIEW(gl.wv));
1104
1105	/* assumes that the inspector has not been detached by the user */
1106	if (webkit_web_inspector_is_attached(i))
1107		webkit_web_inspector_close(i);
1108	else
1109		webkit_web_inspector_show(i);
1110}
1111
1112void
1113toggle_tls_error_policy(void)
1114{
1115	webkit_website_data_manager_set_tls_errors_policy(
1116	    webkit_web_view_get_website_data_manager(WEBKIT_WEB_VIEW(gl.wv)),
1117	    !webkit_website_data_manager_get_tls_errors_policy(
1118	        webkit_web_view_get_website_data_manager(WEBKIT_WEB_VIEW(gl.wv))
1119	    )
1120	);
1121	webkit_web_view_reload_bypass_cache(WEBKIT_WEB_VIEW(gl.wv));
1122}
1123
1124gchar *
1125resolve_uri(
1126	const gchar                                *t)
1127{
1128	gchar                  *u = NULL,           /* uri to return */
1129	                       *l = NULL,           /* temporary string */
1130	                       *e = NULL;           /* uri encoded string */
1131	const gchar            *s = NULL;
1132	wordexp_t               x;                  /* wordexp struct */
1133	int                     r = -1;             /* result holder for wordexp */
1134
1135	/* cannot expand string in file scheme, so try without scheme */
1136	if (!strncmp("file://", t, 7) && (t[7] == '~' || t[7] == '$'))
1137		return resolve_uri(t + 7);
1138
1139	/* use internal about page so that about: prefix is ignored by WebKit */
1140	if (!strncmp(t, "about:", 6) && strcmp(t, ABOUT_BLANK))
1141		u = g_strdup_printf(ABOUT_SCHEME":%s", t + 6);
1142	/* check if valid scheme, and if so just create a copy of the text */
1143	else if ((s = g_uri_peek_scheme(t))
1144	         && g_strv_contains(CFG_L(CFN_URI_SCHEMES), s))
1145		u = g_strdup(t);
1146	/* if no match, then test xdg schemes (schemes that are redirected) */
1147	else if (s && CFG_L(CFN_XDG_SCHEMES)
1148	         && g_strv_contains(CFG_L(CFN_XDG_SCHEMES), s))
1149		xdg_open(s, t, FALSE);
1150	/* if path is local, use the file scheme, else try to see if the string is
1151	 * expandable and is a local path */
1152	else if ((l = realpath(t, NULL)) || ((r = wordexp(t, &x, 0)) == 0
1153	          && (l = resolve_uri_words(x.we_wordc, x.we_wordv))))
1154		u = g_strdup_printf("file://%s", l);
1155	/* else, check if the text can be interpreted as a valid https uri; it's
1156	 * not enough to check if uri is valid - check for period and no spaces */
1157	else if (strchr(t, '.') && !strchr(t, ' ')
1158	         && g_uri_is_valid((l = g_strdup_printf("https://%s", t)), 0, NULL))
1159		u = g_strdup(l);
1160	/* fallback to web search, using a specified search engine */
1161	else
1162		u = g_strdup_printf(
1163		    CFG_S(CFN_SEARCH_ENGINE_URI_FORMAT),
1164		    (e = g_uri_escape_string(t, NULL, FALSE))
1165		);
1166
1167	if (r == 0 || r == WRDE_NOSPACE) /* free on success (r == 0) and NOSPACE */
1168		wordfree(&x);
1169	g_free_all((gpointer[]){e, l, NULL});
1170	return u; /* return a pointer that the caller is responsible for freeing */
1171}
1172
1173char *
1174resolve_uri_words(
1175	int                                         c,
1176	char                                      **w)
1177{
1178	gchar                  *d = NULL,           /* uri decoded string */
1179	                       *p = NULL,           /* path to return */
1180	                       *s = NULL;           /* concatenated string */
1181	size_t                  l,                  /* arbitrary length holder */
1182	                        wl[c];              /* word length */
1183	int                     i;
1184
1185	for (i = 0, l = 0; i < c; l += (wl[i] = strlen(w[i])) + 1, i++);
1186
1187	if (!(s = (char *)malloc(l * sizeof(char))))
1188		die(__NAME__ ": fatal: malloc failed\n");
1189
1190	/* concatenate, or join, words into a space separated string */
1191	for (i = 0, l = 0; i < c; l += wl[i], i++) {
1192		memcpy(s + l, w[i], wl[i]);
1193		memcpy(s + l++ + wl[i], i == c - 1 ? "\0" : " ", 1);
1194	}
1195
1196	p = realpath((d = g_uri_unescape_string(s, NULL)), NULL);
1197
1198	g_free_all((gpointer[]){d, s, NULL});
1199	return p; /* return a pointer that the caller is responsible for freeing */
1200}
1201
1202void
1203update_favicon(void)
1204{
1205	cairo_surface_t        *f;                  /* favicon */
1206	GdkPixbuf              *b,                  /* pix buffer */
1207	                       *s;                  /* pix buffer (scaled) */
1208	int                     d;                  /* scaled dimension */
1209
1210	/* set fallback favicon */
1211	gtk_window_set_icon_name(GTK_WINDOW(gl.win), ICON_GLOBE);
1212
1213	if (!(f = webkit_web_view_get_favicon(WEBKIT_WEB_VIEW(gl.wv))))
1214		return;
1215
1216	if (!(b = gdk_pixbuf_get_from_surface(f, 0, 0,
1217	    cairo_image_surface_get_width(f),
1218	    cairo_image_surface_get_height(f))))
1219		return;
1220
1221	/* setting the window icon enables other programs to retrieve the icon */
1222	d = 16 * gtk_widget_get_scale_factor(gl.win);
1223	s = gdk_pixbuf_scale_simple(b, d, d, GDK_INTERP_BILINEAR);
1224	gtk_window_set_icon(GTK_WINDOW(gl.win), s);
1225
1226	g_object_unref(s);
1227	g_object_unref(b);
1228}
1229
1230void
1231update_load_progress(void)
1232{
1233	gdouble                 p;
1234
1235	p = webkit_web_view_get_estimated_load_progress(WEBKIT_WEB_VIEW(gl.wv));
1236	gtk_entry_set_progress_fraction(GTK_ENTRY(gl.entry), (p == 1 ? 0 : p));
1237}
1238
1239void
1240update_title(void)
1241{
1242	const gchar            *t;
1243	gchar                  *u;
1244
1245	u = get_uri(gl.wv);
1246	t = webkit_web_view_get_title(WEBKIT_WEB_VIEW(gl.wv));
1247
1248	/* title priority: title, url, __NAME__ */
1249	t = !t || t[0] == 0 ? (!u || u[0] == 0 ? __NAME__ : u) : t;
1250
1251	gtk_entry_set_icon_from_icon_name(
1252	    GTK_ENTRY(gl.entry),
1253	    GTK_ENTRY_ICON_PRIMARY,
1254	      gl.failed_crt ? ICON_BAD_TLS
1255	    : gl.https && gl.tls_error != G_TLS_CERTIFICATE_NO_FLAGS ? ICON_BAD_TLS
1256	    : gl.https ? ICON_TLS
1257	    : NULL
1258	);
1259
1260	gtk_window_set_title(GTK_WINDOW(gl.win), t);
1261}
1262
1263void
1264uri_changed(void)
1265{
1266	gchar                  *t = NULL;
1267
1268	/* make sure to not overwrite the "WEB PROCESS CRASHED" message */
1269	if ((t = get_uri(gl.wv)) && strlen(t) > 0) {
1270		gtk_entry_set_text(GTK_ENTRY(gl.entry), t);
1271		save_history(t);
1272	}
1273
1274	g_free(t);
1275}
1276
1277void
1278web_view_crashed(void)
1279{
1280	gchar                  *t = NULL,
1281	                       *u = NULL;
1282
1283	gtk_entry_set_text(
1284	    GTK_ENTRY(gl.entry),
1285	    (t = g_strdup_printf("WEB PROCESS CRASHED: %s", (u = get_uri(gl.wv))))
1286	);
1287	g_free_all((gpointer[]){t, u, NULL});
1288}
1289
1290void
1291xdg_open(
1292	const gchar                                *s,
1293	const gchar                                *t,
1294	gboolean                                    keep)
1295{
1296	char                    b[URI_MAX];         /* command line buffer */
1297
1298	if (fork() == 0) {
1299		/* if not keep, make sure to send the scheme the way it was matched */
1300		sprintf(b, "%s%s", s, keep ? t : t+strlen(s));
1301		execvp(XDG_OPEN, (char *[]){ XDG_OPEN, b, NULL });
1302		fprintf(stderr, "child process (execvp) failed: %s %s\n", XDG_OPEN, b);
1303		exit(EXIT_FAILURE);
1304	}
1305}
1306
1307int
1308main(
1309	int                                         argc,
1310	char                                      **argv)
1311{
1312	int                     opt,
1313	                        i;
1314
1315	sigchld(0);
1316
1317	gl.arg0 = argv[0];
1318	gtk_init(&argc, &argv);
1319
1320	/* load default configuration before reading command-line arguments */
1321	load_configuration();
1322
1323	while ((opt = getopt(argc, argv, "E:")) != -1)
1324		switch (opt) {
1325		case 'E': to_win(&gl.embed, optarg); break;
1326		default:  die("Usage: " __NAME__ " [-E WINID] [URI ...] [FILE ...]\n");
1327		}
1328
1329	set_default_web_context();
1330	main_window_setup();
1331
1332	/* load a default home uri if no clients and no arguments exist  */
1333	if (optind >= argc)
1334		initialize(CFG_S(CFN_HOME_URI));
1335	/* load stdin if first argument is '-' */
1336	if (optind < argc && !strcmp(argv[optind], "-"))
1337		load_stdin();
1338	/* load remaining command line arguments as uris into new clients */
1339	else if (optind < argc)
1340		for (i = optind; i < argc; initialize(argv[i++]));
1341
1342	gtk_main();
1343
1344	exit(EXIT_SUCCESS);
1345}