adji

Adji's a Decisive and Joyful Internet browser
git clone https://noxz.tech/git/adji.git
adji

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(WebKitURISchemeRequest    *request,
25                     gpointer                   data)
26{
27	(void) data;
28
29	GError                 *e;                  /* error holder */
30	GInputStream           *s;                  /* stream to send */
31	GString                *m;                  /* message of stream */
32	const gchar            *p;                  /* path holder eg. 'config' */
33	gchar                 **w;                  /* temporary word holder */
34	gsize                   sl;                 /* size of stream */
35	int                     st[4] = {0};        /* memory stats */
36
37	p = webkit_uri_scheme_request_get_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\">Configurable Name</th>"
45		    "<th align=\"left\">Value</th>"
46		    "</tr>"
47		    "<tr><td>"__NAME__" version</td><td>%s</td></tr>"
48		    "<tr><td>WebKit version</td><td>%u.%u.%u</td></tr>",
49		    VERSION,
50		    webkit_get_major_version(),
51		    webkit_get_minor_version(),
52		    webkit_get_micro_version());
53		for (int i = 0; i < LastConfig; i++) {
54			if (cfg[i].e == NULL)
55				continue;
56			g_string_append_printf(m, "<tr><td>%s</td><td>", cfg[i].e);
57			switch (cfg[i].t) {
58			case CFG_STRING:
59				g_string_append_printf(m, "%s", cfg[i].v.s);
60				break;
61			case CFG_INT:
62				g_string_append_printf(m, "%d", cfg[i].v.i);
63				break;
64			case CFG_FLOAT:
65				g_string_append_printf(m, "%f", cfg[i].v.f);
66				break;
67			case CFG_BOOL:
68				g_string_append_printf(m, "%s", cfg[i].v.b ? "TRUE" : "FALSE");
69				break;
70			case CFG_LIST:
71				for (w = cfg[i].v.l; w && *w; w++)
72					g_string_append_printf(m, "%s%s", w == cfg[i].v.l
73					                       ? "" : "; ", *w);
74				break;
75			}
76			g_string_append_printf(m, "</td></tr>");
77		}
78		g_string_append_printf(m, "</table>");
79		g_string_append_printf(m, "</body></html>");
80	/* about:memory (only exists if memory can be read) */
81	} else if (!strcmp(p, "memory")
82	           && get_memory(&(st[0]), &(st[1]), &(st[2]), &(st[3]))) {
83		g_string_append_printf((m = g_string_new(NULL)), "<html><body>"
84		    "<table style=\"width:100%%;\">"
85		    "<tr><td>current real memory</td><td>%'d kB</tr>"
86		    "<tr><td>peak real memory</td><td>%'d kB</tr>"
87		    "<tr><td>current virt memory</td><td>%'d kB</tr>"
88		    "<tr><td>peak virt memory</td><td>%'d kB</tr>"
89		    "</table>"
90		    "</body></html>",
91		    st[0], st[1], st[2], st[3]
92		);
93	} else {
94		webkit_uri_scheme_request_finish_error(request, (e = g_error_new(
95		    BROWSER_ERROR,
96		    BROWSER_ERROR_INVALID_ABOUT_PATH,
97		    "Invalid 'about:%s' page", p)));
98		g_error_free(e);
99		return;
100	}
101
102	sl = strlen(m->str);
103	s = g_memory_input_stream_new_from_data(m->str, sl, NULL);
104	webkit_uri_scheme_request_finish(request, s, sl, "text/html");
105	g_object_unref(s);
106	g_string_free(m, TRUE);
107}
108
109struct Client*
110client_create(const gchar                      *uri,
111              WebKitWebView                    *related_wv,
112              gboolean                          show,
113              gboolean                          focus_tab)
114{
115	struct Client          *c;                  /* client to be */
116	gchar                  *u = NULL;           /* translated uri */
117	GtkWidget              *e,                  /* event box */
118	                       *t;                  /* tab box */
119	int                     i;
120
121	/* communicate uri through ipc if constructed */
122	if (uri != NULL && gl.ipc == IPC_CLIENT) {
123		if ((u = resolve_uri(uri)) == NULL || ipc_send(u) <= 0
124		    || ipc_send("\n") <= 0)
125			fprintf(stderr, __NAME__": Could not send message '%s'\n", u);
126		g_free(u);
127		return NULL; /* the request was handled elsewhere */
128	}
129
130	/* do not create a new client if existing uri was resolved to NULL, as it
131	 * was handled as an XDG-OPEN event. Still process uri == NULL for "create"
132	 * callback signals, such as "Open Link in New Window". */
133	if (uri != NULL && (u = resolve_uri(uri)) == NULL)
134		return NULL;
135
136	if (!(c = calloc(1, sizeof(struct Client))))
137		die(__NAME__ ": fatal: calloc failed\n");
138
139	if (related_wv == NULL)
140		c->wv = webkit_web_view_new();
141	else
142		c->wv = webkit_web_view_new_with_related_view(related_wv);
143
144	/* connect signal callbacks */
145	CB(c->wv, "button-release-event",           cb_wv_hid, c);
146	CB(c->wv, "close",                          cb_wv_close, c);
147	CB(c->wv, "context-menu",                   cb_context_menu, c);
148	CB(c->wv, "create",                         cb_wv_create, NULL);
149	CB(c->wv, "decide-policy",                  cb_wv_decide_policy, NULL);
150	CB(c->wv, "key-press-event",                cb_wv_hid, c);
151	CB(c->wv, "load-changed",                   cb_wv_load_changed, c);
152	CB(c->wv, "load-failed-with-tls-errors",    cb_wv_tls_load_failed, c);
153	CB(c->wv, "mouse-target-changed",           cb_wv_hover, c);
154	CB(c->wv, "notify::estimated-load-progress",cb_wv_load_progress_changed, c);
155	CB(c->wv, "notify::favicon",                cb_favicon_changed, c);
156	CB(c->wv, "notify::title",                  cb_title_changed, c);
157	CB(c->wv, "notify::uri",                    cb_uri_changed, c);
158	CB(c->wv, "scroll-event",                   cb_wv_hid, c);
159	CB(c->wv, "web-process-crashed",            cb_wv_crashed, c);
160
161	/* load config into webkit settings */
162	c->settings = webkit_settings_new();
163	for (i = 0; i < LastConfig; i++)
164		if (cfg[i].s != NULL)
165			g_object_set(
166			    G_OBJECT(c->settings),
167			    cfg[i].s,
168			    cfg[i].i ? (Arg)!(cfg[i].v.b) : (cfg[i].v),
169			    NULL
170			);
171	webkit_web_view_set_settings(WEBKIT_WEB_VIEW(c->wv), c->settings);
172
173	if (CFG_L(AcceptedLanguages) != NULL)
174		webkit_web_context_set_preferred_languages(
175		    webkit_web_view_get_context(WEBKIT_WEB_VIEW(c->wv)),
176		    CFG_L(AcceptedLanguages)
177		);
178	if (CFG_S(CookieFile) != NULL)
179		webkit_cookie_manager_set_persistent_storage(
180		    webkit_web_context_get_cookie_manager(
181		        webkit_web_view_get_context(WEBKIT_WEB_VIEW(c->wv))
182		    ),
183		    CFG_S(CookieFile),
184		    WEBKIT_COOKIE_PERSISTENT_STORAGE_TEXT
185		);
186	if (CFG_S(ProxyUri) != NULL)
187		webkit_website_data_manager_set_network_proxy_settings(
188		    webkit_web_view_get_website_data_manager(WEBKIT_WEB_VIEW(c->wv)),
189		    WEBKIT_NETWORK_PROXY_MODE_CUSTOM,
190		    webkit_network_proxy_settings_new(
191		        CFG_S(ProxyUri), CFG_L(ProxyIgnore)
192		    )
193		);
194	webkit_web_view_set_zoom_level(
195	    WEBKIT_WEB_VIEW(c->wv), CFG_F(ZoomLevel)
196	);
197
198	/* create entry */
199	c->entry = gtk_entry_new();
200	CB(c->entry, "key-press-event",             cb_entry_hid, c);
201	CB(c->entry, "icon-release",                cb_entry_icon_hid, c);
202	gtk_entry_set_icon_from_icon_name(
203	    GTK_ENTRY(c->entry), GTK_ENTRY_ICON_SECONDARY, CFG_B(DisableJavaScript)
204	    ? ICON_JS_OFF : ICON_JS_ON
205	);
206
207	/* create vertical box to store the web view and the entry */
208	c->vbx = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
209	gtk_box_pack_start(GTK_BOX(c->vbx), c->wv, TRUE, TRUE, 0);
210	gtk_box_pack_start(GTK_BOX(c->vbx), c->entry, FALSE, FALSE, 0);
211	gtk_container_set_focus_child(GTK_CONTAINER(c->vbx), c->wv);
212
213	/* create tab containing an icon and a label */
214	c->tab_icon = gtk_image_new_from_icon_name(ICON_GLOBE, GTK_ICON_SIZE_MENU);
215	c->tab_label = gtk_label_new(__NAME__);
216	gtk_label_set_ellipsize(GTK_LABEL(c->tab_label), PANGO_ELLIPSIZE_END);
217	gtk_widget_set_halign(GTK_WIDGET(c->tab_label), GTK_ALIGN_START);
218	gtk_widget_set_hexpand(GTK_WIDGET(c->tab_label), TRUE);
219	gtk_widget_set_has_tooltip(GTK_WIDGET(c->tab_label), TRUE);
220	/* pack icon and label in a horizontal box */
221	t = gtk_box_new(
222	    GTK_ORIENTATION_HORIZONTAL, 5 * gtk_widget_get_scale_factor(mw.win)
223	);
224	gtk_box_pack_start(GTK_BOX(t), c->tab_icon, FALSE, FALSE, 0);
225	gtk_box_pack_start(
226	    GTK_BOX(t),
227	    c->tab_label,
228	    TRUE,
229	    TRUE,
230	    3 * gtk_widget_get_scale_factor(mw.win)
231	);
232	/* back the tab inside an event box to enable scroll and button events */
233	e = gtk_event_box_new();
234	gtk_container_add(GTK_CONTAINER(e), t);
235	gtk_widget_add_events(e, GDK_SCROLL_MASK);
236	CB(e, "button-release-event",               cb_tab_hid, c);
237	CB(e, "scroll-event",                       cb_tab_hid, c);
238	/* store a (non client) reference to the label for later use */
239	g_object_set_data(G_OBJECT(e), __NAME__"-tab-label", c->tab_label);
240	/* show tab */
241	gtk_widget_show_all(e);
242
243	/* append everything reorderable to the notebook after current page */
244	gtk_notebook_insert_page(
245	    GTK_NOTEBOOK(mw.nb),
246	    c->vbx,
247	    e,
248	    gtk_notebook_get_current_page(GTK_NOTEBOOK(mw.nb)) + 1
249	);
250	gtk_notebook_set_tab_reorderable(GTK_NOTEBOOK(mw.nb), c->vbx, TRUE);
251
252	/* manage which tab should be focused */
253	c->focus_new_tab = focus_tab;
254	if (show)
255		show_web_view(c);
256	else
257		CB(c->wv, "ready-to-show",              cb_wv_show, c);
258
259	/* finally load the uri */
260	if (u != NULL)
261		webkit_web_view_load_uri(WEBKIT_WEB_VIEW(c->wv), u);
262	g_free(u);
263
264	gl.clients++;
265	return c;
266}
267
268void
269client_destroy(struct Client *c)
270{
271	int                     i;
272
273	/* disconnect all handlers */
274	g_signal_handlers_disconnect_by_data(G_OBJECT(c->wv), c);
275	g_signal_handlers_disconnect_by_data(G_OBJECT(c->wv), NULL);
276
277	if ((i = gtk_notebook_page_num(GTK_NOTEBOOK(mw.nb), c->vbx)) != -1)
278		gtk_notebook_remove_page(GTK_NOTEBOOK(mw.nb), i);
279	else
280		fprintf(stderr, __NAME__": Page does not exist in notebook\n");
281
282	free(c);
283
284	if (--gl.clients == 0)
285		quit();
286}
287
288gboolean
289command(struct Client                          *c,
290        const gchar                            *t)
291{
292	if (t[0] == '/') {          /* in-page search */
293		g_free(gl.search_text);
294		gl.search_text = g_strdup(t + 1);
295		search(c, IPS_INIT);
296		return TRUE;
297	} else if (t[0] == 'q') {   /* quit (vim-like) */
298		quit();
299		return TRUE;
300	}
301
302	return FALSE;
303}
304
305void
306create_context_menu(struct Client              *c,
307                    WebKitContextMenu          *context_menu,
308                    WebKitHitTestResult        *hit_test_result)
309{
310	guint                   x;                  /* hit test result context */
311
312	x = webkit_hit_test_result_get_context(hit_test_result);
313
314	webkit_context_menu_prepend(
315	    context_menu, webkit_context_menu_item_new_separator()
316	);
317
318	/* if document is the only context */
319	if (x == WEBKIT_HIT_TEST_RESULT_CONTEXT_DOCUMENT) {
320		create_context_menu_item(
321		    c,
322		    context_menu,
323		    "open-external",
324		    "Open Page Externally",
325		    cb_open_external
326		);
327		return; /* no need to check further */
328	}
329
330	if (x & WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK)
331		create_context_menu_item(
332		    c,
333		    context_menu,
334		    "open-external",
335		    "Open Link Externally",
336		    cb_open_external
337		);
338
339	/* requires javascript for DOM access from here on */
340	if (!webkit_settings_get_enable_javascript(c->settings))
341		return;
342
343	if (x & WEBKIT_HIT_TEST_RESULT_CONTEXT_SELECTION)
344		create_context_menu_item(
345		    c,
346		    context_menu,
347		    "selection-search",
348		    "Search Selection",
349		    cb_selection_search
350		);
351}
352
353void
354create_context_menu_item(struct Client          *c,
355                         WebKitContextMenu      *context_menu,
356                         const gchar            *name,
357                         const gchar            *label,
358                         void                   *action)
359{
360	GAction                *a = (GAction*)g_simple_action_new(name, NULL);
361
362	CB(a, "activate", action, c);
363	webkit_context_menu_prepend(
364	    context_menu,
365	    webkit_context_menu_item_new_from_gaction(a, label, NULL)
366	);
367	g_object_unref(a);
368}
369
370void
371die(const char *msg)
372{
373	fprintf(stderr, msg);
374	exit(EXIT_FAILURE);
375}
376
377int
378get_memory(int                                 *cr, /* current real memory */
379           int                                 *pr, /* peak real memory */
380           int                                 *cv, /* current virtual memory */
381           int                                 *pv) /* peak virtual memory */
382{
383	char                    b[256] = {0};
384	FILE                   *f;
385
386	*cr = *pr = *cv = *pv = -1;
387
388	if (!(f = fopen("/proc/self/status", "r")))
389		return 0;
390
391	while (fscanf(f, " %255s", b) == 1)
392		if (!strcmp(b, "VmRSS:"))
393			fscanf(f, " %d", cr);
394		else if (!strcmp(b, "VmHWM:"))
395			fscanf(f, " %d", pr);
396		else if (!strcmp(b, "VmSize:"))
397			fscanf(f, " %d", cv);
398		else if (!strcmp(b, "VmPeak:"))
399			fscanf(f, " %d", pv);
400	fclose(f);
401
402	return (*cr != -1 && *pr != -1 && *cv != -1 && *pv != -1);
403}
404
405gchar*
406get_uri(GtkWidget* wv)
407{
408	const gchar *u;
409
410	if (!(u = webkit_web_view_get_uri(WEBKIT_WEB_VIEW(wv))))
411		return NULL;
412
413	if (!strncmp(u, ABOUT_SCHEME":", strlen(ABOUT_SCHEME":")))
414		return g_strconcat("about:", u + strlen(ABOUT_SCHEME":"), NULL);
415
416	return g_strdup(u);
417}
418
419gboolean
420ipc_request(GIOChannel                         *src,
421            GIOCondition                        condition,
422            gpointer                            data)
423{
424	gchar                  *u = NULL;           /* uri received */
425
426	(void)condition;        /* not used */
427	(void)data;             /* not used */
428
429	g_io_channel_read_line(src, &u, NULL, NULL, NULL);
430	if (u) {
431		g_strstrip(u);
432		client_create(u, NULL, TRUE, TRUE);
433		g_free(u);
434	}
435	return TRUE;
436}
437
438ssize_t
439ipc_send(char *data)
440{
441	size_t                  s = 0,              /* data amount sent */
442	                        l = strlen(data);   /* data length */
443	ssize_t                 w;                  /* data written */
444
445	while (s < l) { /* write until everything is sent */
446		if ((w = write(gl.ipc_pipe_fd, data + s, l - s)) == -1) {
447			if (errno == EINTR)
448				continue;       /* EINTR: so retry */
449			perror(__NAME__": Error writing to fifo");
450			return w;
451		} else if (w == 0) {
452			return 0;
453		}
454		s += w;
455	}
456
457	return s;
458}
459
460void
461ipc_setup(void)
462{
463	gchar                  *f = NULL,           /* fifo file name */
464	                       *p = NULL;           /* fifo file path */
465
466	/* only setup for ipc clients */
467	if (gl.ipc != IPC_CLIENT)
468		return;
469
470	/* create fifo file path and allow for several ipc setups by naming them */
471	f = g_strdup_printf("%s_%s.fifo", __NAME__, CFG_S(FifoName));
472	p = g_build_filename(g_get_user_runtime_dir(), f, NULL);
473
474	/* fail hard if fifo cannot be created */
475	if (!g_file_test(p, G_FILE_TEST_EXISTS) && mkfifo(p, 0600) == -1)
476		die(__NAME__ ": fatal: mkfifo failed\n");
477
478	/* if no one is listening create the ipc host instance */
479	if ((gl.ipc_pipe_fd = open(p, O_WRONLY | O_NONBLOCK)) == -1) {
480		g_io_add_watch(
481		    g_io_channel_new_file(p, "r+", NULL),
482		    G_IO_IN,
483		    (GIOFunc)ipc_request,
484		    NULL
485		);
486		gl.ipc = IPC_HOST;  /* convert client to host */
487	}
488
489	g_free(f);
490	g_free(p);
491}
492
493gboolean
494key_common(struct Client                       *c,
495           GdkEventKey                         *event)
496{
497	int                     i,
498	                        m;
499	gchar                  *u = NULL;
500
501	/* only handle key presses using the alt key */
502	if (!(event->state & GDK_MOD1_MASK))
503		return FALSE;
504
505	/* key presses using ctrl+alt keys */
506	if (event->state & GDK_CONTROL_MASK) {
507		switch (event->keyval) {
508		case GDK_KEY_J:
509		case GDK_KEY_K: /* move page/tab backwards or forwards in stack */
510			i = gtk_notebook_page_num(GTK_NOTEBOOK(mw.nb), c->vbx);
511			m = gtk_notebook_get_n_pages(GTK_NOTEBOOK(mw.nb)) - 1;
512			i += event->keyval == GDK_KEY_J
513			    ? i == 0 ? 0 : -1
514			    : i == m ? m : 1;
515			gtk_notebook_reorder_child(GTK_NOTEBOOK(mw.nb), c->vbx, i);
516			return TRUE;
517		case GDK_KEY_j: /* go to previous page/tab */
518			gtk_notebook_prev_page(GTK_NOTEBOOK(mw.nb));
519			return TRUE;
520		case GDK_KEY_k: /* go to next page/tab */
521			gtk_notebook_next_page(GTK_NOTEBOOK(mw.nb));
522			return TRUE;
523		}
524	}
525
526	switch (event->keyval) {
527	case GDK_KEY_q: /* destroy client and close tab */
528		client_destroy(c);
529		return TRUE;
530	case GDK_KEY_w: /* go to home page */
531		if ((u = resolve_uri(CFG_S(HomeUri))) != NULL)
532			webkit_web_view_load_uri(WEBKIT_WEB_VIEW(c->wv), u);
533		g_free(u);
534		return TRUE;
535	case GDK_KEY_t: /* create new client/tab */
536		if ((u = resolve_uri(CFG_S(HomeUri))) != NULL) {
537			c = client_create(u, NULL, TRUE, TRUE);
538			g_free(u);
539			gtk_widget_grab_focus(c->entry);
540		}
541		return TRUE;
542	case GDK_KEY_r: /* reload, while bypassing cache */
543		webkit_web_view_reload_bypass_cache(WEBKIT_WEB_VIEW(c->wv));
544		return TRUE;
545	case GDK_KEY_i:
546		toggle_inspector(c);
547		return TRUE;
548	case GDK_KEY_o: /* focus entry */
549		gtk_widget_grab_focus(c->entry);
550		return TRUE;
551	case GDK_KEY_n: /* search forward */
552		search(c, IPS_FORWARD);
553		return TRUE;
554	case GDK_KEY_N: /* search backward */
555		search(c, IPS_BACKWARD);
556		return TRUE;
557	case GDK_KEY_slash: /* initiate in-page search */
558		gtk_widget_grab_focus(c->entry);
559		gtk_entry_set_text(GTK_ENTRY(c->entry), ":/");
560		gtk_editable_set_position(GTK_EDITABLE(c->entry), -1);
561		return TRUE;
562	case GDK_KEY_b: /* toggle tab visibility */
563		gtk_notebook_set_show_tabs(
564		    GTK_NOTEBOOK(mw.nb),
565		    !gtk_notebook_get_show_tabs(GTK_NOTEBOOK(mw.nb))
566		);
567		return TRUE;
568	case GDK_KEY_1:
569	case GDK_KEY_2:
570	case GDK_KEY_3:
571	case GDK_KEY_4:
572	case GDK_KEY_5:
573	case GDK_KEY_6:
574	case GDK_KEY_7:
575	case GDK_KEY_8:
576	case GDK_KEY_9: /* go to nth tab */
577		gtk_notebook_set_current_page(GTK_NOTEBOOK(mw.nb), event->keyval-0x31);
578		return TRUE;
579	case GDK_KEY_0:
580	case GDK_KEY_minus:
581	case GDK_KEY_equal: /* set zoom level */
582		webkit_web_view_set_zoom_level(
583		    WEBKIT_WEB_VIEW(c->wv),
584		    event->keyval == GDK_KEY_0
585		    ? CFG_F(ZoomLevel)
586		    : webkit_web_view_get_zoom_level(WEBKIT_WEB_VIEW(c->wv))
587		    + ((event->keyval == GDK_KEY_minus) ? -0.1 : 0.1)
588		);
589		return TRUE;
590	case GDK_KEY_H: /* go back in history */
591		webkit_web_view_go_back(WEBKIT_WEB_VIEW(c->wv));
592		return TRUE;
593	case GDK_KEY_L: /* go forward in history */
594		webkit_web_view_go_forward(WEBKIT_WEB_VIEW(c->wv));
595		return TRUE;
596	case GDK_KEY_s: /* toggle javascript (on or off) */
597		set_javascript_policy(c, JSP_TOGGLE);
598		return TRUE;
599	case GDK_KEY_c: /* toggle tls error policy (ignore or fail) */
600		toggle_tls_error_policy(c);
601		return TRUE;
602	case GDK_KEY_h:
603	case GDK_KEY_j:
604	case GDK_KEY_k:
605	case GDK_KEY_l: /* in-page movement by emulating arrow keys... */
606	case GDK_KEY_g:
607	case GDK_KEY_G: /* ... as well as home and end */
608		event->keyval /* translate key press accordingly */
609		    = event->keyval == GDK_KEY_h ? GDK_KEY_Left
610		    : event->keyval == GDK_KEY_j ? GDK_KEY_Down
611		    : event->keyval == GDK_KEY_k ? GDK_KEY_Up
612		    : event->keyval == GDK_KEY_l ? GDK_KEY_Right
613		    : event->keyval == GDK_KEY_g ? GDK_KEY_Home
614		    : /*                        */ GDK_KEY_End;
615		event->state = 0;
616		gtk_propagate_event(GTK_WIDGET(c->wv), (GdkEvent*)event);
617		return TRUE;
618	case GDK_KEY_p: /* open print dialog */
619		webkit_print_operation_run_dialog(
620		    webkit_print_operation_new(WEBKIT_WEB_VIEW(c->wv)),
621		    GTK_WINDOW(mw.win)
622		);
623		return TRUE;
624	}
625
626	/* check external handler keys */
627	if (cfg[ExternalHandlerKeys].v.b &&
628	    event->keyval < 256 &&
629	    g_strv_contains(
630	        CFG_L(ExternalHandlerKeys), (gchar[]){(gchar)event->keyval, 0}
631	    )) {
632		open_external(c, (gchar)event->keyval);
633		return TRUE;
634	}
635
636	return FALSE;
637}
638
639gboolean
640key_entry(struct Client                        *c,
641          GdkEventKey                          *event)
642{
643	const gchar            *t;
644	gchar                  *u = NULL,
645	                       *l = NULL;
646	int                     p,
647	                        m;
648
649	/* handle key presses using the alt key (takes precedence over common) */
650	if (event->state & GDK_MOD1_MASK) {
651		switch (event->keyval) { /* movements inside entry */
652		case GDK_KEY_l:
653		case GDK_KEY_h:
654			p = gtk_editable_get_position(GTK_EDITABLE(c->entry));
655			m = gtk_entry_get_text_length(GTK_ENTRY(c->entry));
656			p += event->keyval == GDK_KEY_l
657				? m > p ? 1 : 0     /* move right */
658				: 0 < p ? -1 : 0;   /* move left */
659			gtk_editable_set_position(GTK_EDITABLE(c->entry), p);
660			return TRUE;
661		}
662	}
663
664	/* handle common key presses */
665	if (key_common(c, event))
666		return TRUE;
667
668	/* handle any key press (not just using the alt key) */
669	switch (event->keyval) {
670	case GDK_KEY_KP_Enter:
671	case GDK_KEY_Return:
672		gtk_widget_grab_focus(c->wv);
673		if ((t = gtk_entry_get_text(GTK_ENTRY(c->entry))) == NULL)
674			return TRUE;
675		if (t[0] != ':') { /* if not a command */
676			/* store current uri before loading new uri */
677			l = get_uri(c->wv);
678			if ((u = resolve_uri(t)) != NULL)
679				webkit_web_view_load_uri(WEBKIT_WEB_VIEW(c->wv), u);
680			g_free(u);
681			/* fix: notify::uri won't be raised if current uri is unchanged */
682			if (!strcmp(l, (u = get_uri(c->wv))))
683				uri_changed(c);
684			g_free(u);
685			g_free(l);
686			return TRUE;
687		} else if (command(c, t + 1)) { /* if command */
688			return TRUE;
689		} /* FALL THROUGH */
690	case GDK_KEY_Escape:
691		gtk_widget_grab_focus(c->wv);
692		u = get_uri(c->wv);
693		gtk_entry_set_text(GTK_ENTRY(c->entry), (u == NULL ? __NAME__ : u));
694		gtk_editable_set_position(GTK_EDITABLE(c->entry), -1);
695		g_free(u);
696		return TRUE;
697	}
698
699	return FALSE;
700}
701
702gboolean
703key_tab(struct Client                          *c,
704        GdkEvent                               *event)
705{
706	GdkScrollDirection      d;
707
708	if (event->type == GDK_BUTTON_RELEASE) {
709		if (((GdkEventButton *)event)->button == 2) { /* scroll wheel click */
710			client_destroy(c);
711			return TRUE;
712		}
713	} else if (event->type == GDK_SCROLL) { /* scroll wheel selection */
714		gdk_event_get_scroll_direction(event, &d);
715		if (d == GDK_SCROLL_UP)
716			gtk_notebook_next_page(GTK_NOTEBOOK(mw.nb));
717		else if (d == GDK_SCROLL_DOWN)
718			gtk_notebook_prev_page(GTK_NOTEBOOK(mw.nb));
719		return TRUE;
720	}
721
722	/* regain lost focus while clicking a tab (not a perfect solution)
723	 * note: this won't work if the cursor is dragged while a button is
724	 *   pressed. One pseudo solution for this would be to subscribe to
725	 *   event-after (specifically the GDK_ENTER_NOTIFY event type) on tab hid
726	 *   events, however this only works if the selected tab is hovered while a
727	 *   button is released and would also cause the corresponding web view for
728	 *   any hovering tab to grab focus (effectively stealing focus from the
729	 *   current web view or the entry when not desirable). */
730	gtk_widget_grab_focus(c->wv);
731	gtk_editable_set_position(GTK_EDITABLE(c->entry), -1);
732
733	return FALSE;
734}
735
736gboolean
737key_web_view(struct Client                     *c,
738             GdkEvent                          *event)
739{
740	gdouble                 dx,                 /* scroll x-delta */
741	                        dy;                 /* scroll y-delta */
742
743	/* handle common key presses */
744	if (event->type == GDK_KEY_PRESS && key_common(c, (GdkEventKey *)event))
745		return TRUE;
746
747	/* escape key: stop web rendering */
748	if (event->type == GDK_KEY_PRESS
749	    && ((GdkEventKey *)event)->keyval == GDK_KEY_Escape) {
750		webkit_web_view_stop_loading(WEBKIT_WEB_VIEW(c->wv));
751		gtk_entry_set_progress_fraction(GTK_ENTRY(c->entry), 0);
752	/* mouse button events */
753	} else if (event->type == GDK_BUTTON_RELEASE) {
754		switch (((GdkEventButton *)event)->button) {
755		case 2:  /* mouse middle button: open in new tab */
756			if (c->hover_uri != NULL) {
757				client_create(c->hover_uri, NULL, TRUE, FALSE);
758				return TRUE;
759			}
760			break;
761		case 8:  /* mouse button back: go back in history */
762			webkit_web_view_go_back(WEBKIT_WEB_VIEW(c->wv));
763			return TRUE;
764		case 9:  /* mouse button forward: go forward in history */
765			webkit_web_view_go_forward(WEBKIT_WEB_VIEW(c->wv));
766			return TRUE;
767		}
768	/* scroll using alt pressed: zoom in or out */
769	} else if (event->type == GDK_SCROLL
770	           && (((GdkEventScroll *)event)->state & GDK_MOD1_MASK)) {
771		gdk_event_get_scroll_deltas(event, &dx, &dy);
772		webkit_web_view_set_zoom_level(
773		    WEBKIT_WEB_VIEW(c->wv),
774		    dx != 0
775		    ? CFG_F(ZoomLevel)
776		    : webkit_web_view_get_zoom_level(WEBKIT_WEB_VIEW(c->wv)) + -dy * 0.1
777		);
778		return TRUE;
779	}
780
781	return FALSE;
782}
783
784void
785load_changed(struct Client                     *c,
786             WebKitLoadEvent                    event_type)
787{
788	switch (event_type) {
789	/* when page load starts, reset everything */
790	case WEBKIT_LOAD_STARTED:
791		c->https = FALSE;
792		if (c->error_page)
793			c->error_page = FALSE;
794		else
795			g_clear_object(&c->failed_crt);
796		break;
797	/* when page load is redirected, continue as usual */
798	case WEBKIT_LOAD_REDIRECTED:
799		break;
800	/* when page load is committed, get https and tls state */
801	case WEBKIT_LOAD_COMMITTED:
802		c->https = webkit_web_view_get_tls_info(
803		    WEBKIT_WEB_VIEW(c->wv), &c->crt, &c->tls_error
804		);
805		break;
806	/* when page load is finished, run all user scripts */
807	case WEBKIT_LOAD_FINISHED:
808		load_user_styles(WEBKIT_WEB_VIEW(c->wv));
809		run_user_scripts(WEBKIT_WEB_VIEW(c->wv));
810		break;
811	}
812
813	update_title(c);
814}
815
816void
817load_configuration(void)
818{
819	const gchar            *e;                  /* environment variable name */
820	int                     i;
821
822	/* set global defaults */
823	gl.clients              = 0;                /* client counter */
824	gl.ipc                  = IPC_CLIENT;       /* ipc type */
825	gl.ipc_pipe_fd          = 0;                /* ipc pipe file descriptor */
826	gl.search_text          = NULL;             /* in page search phrase */
827	gl.state_lock           = TRUE;             /* prevents state save */
828
829	/* load default configuration */
830	for (i = 0; i < LastConfig; i++) {
831		if (cfg[i].e && (e = g_getenv(cfg[i].e)) != NULL) {
832			switch (cfg[i].t) {
833			case CFG_INT:       cfg[i].v.i = atoi(e); break;
834			case CFG_FLOAT:     cfg[i].v.f = atof(e); break;
835			case CFG_BOOL:      cfg[i].v.b = TRUE; break;
836			case CFG_STRING:    cfg[i].v.s = g_strdup(e); break;
837			case CFG_LIST:      cfg[i].v.l = g_strsplit(e, ",", -1); break;
838			}
839		}
840	}
841}
842
843void
844load_state(void)
845{
846	char                    u[URI_MAX];         /* line/uri holder */
847	FILE                   *fp = NULL;          /* file pointer */
848	int                     c,                  /* character holder */
849	                        x,                  /* state indicators */
850	                        idx = 0;            /* current page index */
851	unsigned                i = 0u;
852
853	/* only load states for ipc host */
854	if (CFG_S(StateFile) == NULL || gl.ipc != IPC_HOST)
855		return;
856
857	if (!(fp = fopen(CFG_S(StateFile), "r"))) {
858		/* fail if file exists as it cannot be accessed, else remove state lock
859		 * to see if saving the state will create it (ie. the parent exists) */
860		if (access(CFG_S(StateFile), F_OK) == 0)
861			perror(__NAME__": Error opening state file");
862		else
863			gl.state_lock = FALSE;
864		return;
865	}
866
867	do {
868		c = fgetc(fp);
869
870		if (i == URI_MAX && !(c == '\n' || c == EOF)) { /* uri is too long */
871			i = 0u;
872			while ((c = fgetc(fp)) != '\n' && c != EOF);
873			if (c == EOF)
874				break;
875		}
876
877		if (c != '\n' && c != EOF) {
878			u[i++] = (char)c;
879			continue;   /* character is added to string, not more to do */
880		}
881
882		if (i == 0)
883			continue;   /* skip empty lines */
884
885		u[i] = '\0';
886		i = 0u;
887
888		if (sscanf(u, "%d:%s", &x, u) != 2)
889			continue;   /* incorrect format, so skip it */
890
891		set_javascript_policy(
892		    client_create(u, NULL, TRUE, TRUE),
893		    (x & STATE_JAVASCRIPT) != 0
894		);
895		idx = x & STATE_CURRENT ? gl.clients - 1 : idx;
896
897	} while (c != EOF);
898
899	fclose(fp);
900
901	/* select current tab if any */
902	if (gl.clients > 0)
903		gtk_notebook_set_current_page(GTK_NOTEBOOK(mw.nb), idx);
904
905	/* enable state save */
906	gl.state_lock = FALSE;
907
908}
909
910void
911load_stdin()
912{
913	int                     c;                  /* character holder */
914	GString                *s = g_string_new(NULL);
915
916	/* read until end of stream */
917	while ((c = getc(stdin)) != EOF)
918		g_string_append_c(s, (gchar)c);
919
920	/* ignore content of stdin if instance is not independent, as there is no
921	 * uri to send over IPC anyway */
922	if (gl.ipc != IPC_NONE)
923		fprintf(stderr, __NAME__": stdin detected, use with -I instead\n");
924	else
925		/* load the html content, which won't survive a reload */
926		webkit_web_view_load_html(
927		    WEBKIT_WEB_VIEW(client_create(NULL, NULL, TRUE, TRUE)->wv),
928		    s->str,
929		    NULL
930		);
931	g_string_free(s, TRUE);
932}
933
934void
935load_user_styles(WebKitWebView *web_view)
936{
937	gchar                  *c = NULL,           /* file content */
938	                       *p = NULL,           /* path (file) */
939	                       *b = NULL;           /* base directory (scripts) */
940	const gchar            *e = NULL;           /* directory file entry */
941	GDir                   *s = NULL;           /* user style directory */
942
943	b = g_build_filename(g_get_user_config_dir(), __NAME__, CFG_S(UcDir), NULL);
944	if ((s = g_dir_open(b, 0, NULL)) == NULL) {
945		g_free(b);
946		return;
947	}
948
949	webkit_user_content_manager_remove_all_style_sheets(
950	    webkit_web_view_get_user_content_manager(web_view)
951	);
952
953	while ((e = g_dir_read_name(s)) != NULL) {
954		if (g_str_has_suffix((p = g_build_filename(b, e, NULL)), ".css")
955		    && g_file_get_contents(p, &c, NULL, NULL)) {
956			webkit_user_content_manager_add_style_sheet(
957			    webkit_web_view_get_user_content_manager(web_view),
958			    webkit_user_style_sheet_new(
959			        c,
960			        WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
961			        WEBKIT_USER_STYLE_LEVEL_USER,
962			        NULL,
963			        NULL
964			    )
965			);
966			g_free(c);
967		}
968		g_free(p);
969	}
970
971	g_dir_close(s);
972	g_free(b);
973}
974
975void
976main_window_setup(void)
977{
978	GtkWidget              *v;                  /* vertical box */
979
980	/* define window elements */
981	mw.win                  = gtk_window_new(GTK_WINDOW_TOPLEVEL);
982	mw.nb                   = gtk_notebook_new();
983	mw.dbx                  = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
984	v                       = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
985
986	/* window settings */
987	gtk_window_set_default_size(GTK_WINDOW(mw.win), 800, 600);
988	gtk_window_set_title(GTK_WINDOW(mw.win), __NAME__);
989
990	/* notebook settings */
991	gtk_notebook_set_scrollable(GTK_NOTEBOOK(mw.nb), TRUE);
992	gtk_notebook_set_tab_pos(GTK_NOTEBOOK(mw.nb), GTK_POS_TOP);
993
994	/* prevent notebook (including tabs) from stealing focus */
995	gtk_widget_set_can_focus(mw.nb, FALSE);
996
997	/* pack everything in a vertical box */
998	gtk_box_pack_start(GTK_BOX(v), mw.nb, TRUE, TRUE, 0);
999	gtk_box_pack_start(GTK_BOX(v), mw.dbx, FALSE, FALSE, 0);
1000	gtk_container_add(GTK_CONTAINER(mw.win), v);
1001
1002	/* connect signal callbacks */
1003	CB(mw.win,              "destroy",          cb_quit, NULL);
1004	CB(mw.nb,               "switch-page",      cb_notebook_switch_page, NULL);
1005	CBA(mw.nb,              "switch-page",      cb_notebook_modified, NULL);
1006	CB(mw.nb,               "page-added",       cb_notebook_modified, NULL);
1007	CB(mw.nb,               "page-removed",     cb_notebook_modified, NULL);
1008	CB(mw.nb,               "page-reordered",   cb_notebook_modified, NULL);
1009}
1010
1011void
1012open_external(struct Client *c, const gchar s)
1013{
1014	char                    b[URI_MAX];         /* command line buffer */
1015	gchar                  *p = NULL;           /* external handler path */
1016	p = g_build_filename(
1017	    g_get_user_config_dir(), __NAME__, CFG_S(ExternalHandlerFile), NULL
1018	);
1019	if (s)
1020		sprintf(b, "%s %c:%s &", p, s, gtk_entry_get_text(GTK_ENTRY(c->entry)));
1021	else
1022		sprintf(b, "%s %s &", p, gtk_entry_get_text(GTK_ENTRY(c->entry)));
1023	g_free(p);
1024
1025	system(b);
1026}
1027
1028void
1029prepare_download(WebKitDownload                *d,
1030                 gchar                         *suggested_filename)
1031{
1032	gchar                  *s = g_strdup(suggested_filename),
1033	                       *p = NULL,           /* proposed path */
1034	                       *f = NULL,           /* final path */
1035	                       *u = NULL;           /* uri */
1036	GtkWidget              *b;                  /* button */
1037	int                     i;
1038	static gboolean         c = FALSE;          /* condition, TRUE=finished */
1039
1040	/* make suggested filename safe, by replacing directory separators */
1041	for (i = 0; s[i]; i++)
1042		if (s[i] == G_DIR_SEPARATOR)
1043			s[i] = '_';
1044
1045	p = g_build_filename(CFG_S(DownloadDirectory), s, NULL);
1046	f = g_strdup(p);
1047
1048	/* check for a free suffix */
1049	for (i = 1; g_file_test(f, G_FILE_TEST_EXISTS) && i < SUFFIX_MAX; i++) {
1050		g_free(f);
1051		f = rebuild_filename(p, i);
1052	}
1053
1054	if (i < SUFFIX_MAX) {
1055		u = g_filename_to_uri(f, NULL, NULL);
1056		webkit_download_set_destination(d, u);
1057
1058		b = gtk_button_new_with_label(s);
1059		gtk_button_set_always_show_image(GTK_BUTTON(b), TRUE);
1060		gtk_button_set_image(
1061		    GTK_BUTTON(b),
1062		    gtk_image_new_from_icon_name(ICON_DOWNLOAD, GTK_ICON_SIZE_BUTTON)
1063		);
1064		gtk_widget_set_margin_end(b, 1);
1065		gtk_box_pack_start(GTK_BOX(mw.dbx), b, FALSE, FALSE, 0);
1066		gtk_box_reorder_child(GTK_BOX(mw.dbx), b, 0);
1067		gtk_widget_show_all(mw.dbx);
1068
1069		g_object_set_data(G_OBJECT(b), __NAME__"-finished", (gpointer)&c);
1070
1071		CB(d, "notify::estimated-progress", cb_download_changed_progress, b);
1072		CB(d, "finished", cb_download_finished, b);
1073		g_object_ref(d);
1074		CB(b, "button-press-event", cb_download_press, d);
1075	} else { /* could not find a free suffix under the SUFFIX_MAX limit */
1076		fprintf(stderr, __NAME__": Limit reached for filename suffix\n");
1077		webkit_download_cancel(d);
1078	}
1079
1080	g_free(p);
1081	g_free(s);
1082	g_free(f);
1083	g_free(u);
1084}
1085
1086void
1087quit(void)
1088{
1089	save_state();
1090	gtk_main_quit();
1091}
1092
1093gchar *
1094rebuild_filename(
1095	gchar                                      *p,
1096	int                                         n)
1097{
1098	int i, l = strlen(p);
1099	gchar *f;
1100
1101	for (i = l; i >= 0; i--)
1102		if (p[i] == '.' || (p[i] == '/' && (i = l) >= 0))
1103			break;
1104
1105	if (i == -1)
1106		die(__NAME__ ": fatal: invalid download path\n");
1107
1108	if (i < l) {
1109		p[i] = '\0';
1110		f = g_strdup_printf("%s.%d.%s", p, n, &p[i + 1]);
1111		p[i] = '.';
1112	} else {
1113		f = g_strdup_printf("%s.%d", p, n);
1114	}
1115
1116	return f;
1117}
1118
1119void
1120render_tls_error(struct Client                 *c,
1121                 gchar                         *uri,
1122                 GTlsCertificate               *crt,
1123                 GTlsCertificateFlags           cert_flags)
1124{
1125	GString                *m = NULL;           /* message (error) */
1126	gchar                  *h = NULL,           /* html code */
1127	                       *s = NULL,           /* subject name */
1128	                       *i = NULL,           /* issuer name */
1129	                       *b = NULL,           /* not before date */
1130	                       *a = NULL,           /* not after date */
1131	                       *p = NULL;           /* pem block */
1132	GDateTime              *bd = NULL,          /* not before date */
1133	                       *ad = NULL;          /* not after date */
1134
1135	m                       = g_string_new(NULL);
1136	c->failed_crt           = g_object_ref(crt);
1137	c->tls_error            = cert_flags;
1138	c->error_page           = TRUE;
1139
1140	/* translate all flags to messages */
1141	if (c->tls_error & G_TLS_CERTIFICATE_UNKNOWN_CA)
1142		g_string_append(m, MSG_TLS_CERTIFICATE_UNKNOWN_CA);
1143	if (c->tls_error & G_TLS_CERTIFICATE_BAD_IDENTITY)
1144		g_string_append(m, MSG_TLS_CERTIFICATE_BAD_IDENTITY);
1145	if (c->tls_error & G_TLS_CERTIFICATE_NOT_ACTIVATED)
1146		g_string_append(m, MSG_TLS_CERTIFICATE_NOT_ACTIVATED);
1147	if (c->tls_error & G_TLS_CERTIFICATE_EXPIRED)
1148		g_string_append(m, MSG_TLS_CERTIFICATE_EXPIRED);
1149	if (c->tls_error & G_TLS_CERTIFICATE_REVOKED)
1150		g_string_append(m, MSG_TLS_CERTIFICATE_REVOKED);
1151	if (c->tls_error & G_TLS_CERTIFICATE_INSECURE)
1152		g_string_append(m, MSG_TLS_CERTIFICATE_INSECURE);
1153	if (c->tls_error & G_TLS_CERTIFICATE_GENERIC_ERROR)
1154		g_string_append(m, MSG_TLS_CERTIFICATE_GENERIC_ERROR);
1155
1156	/* construct html code and load it */
1157	g_object_get(crt, "subject-name", &s, NULL);
1158	g_object_get(crt, "issuer-name", &i, NULL);
1159	g_object_get(crt, "not-valid-before", &bd, NULL);
1160	g_object_get(crt, "not-valid-after", &ad, NULL);
1161	g_object_get(crt, "certificate-pem", &p, NULL);
1162	b = g_date_time_format_iso8601(bd);
1163	a = g_date_time_format_iso8601(ad);
1164	h = g_strdup_printf(TLS_MSG_FORMAT, uri, m->str, s, i, b, a, p);
1165	webkit_web_view_load_alternate_html(WEBKIT_WEB_VIEW(c->wv), h, uri, NULL);
1166
1167	g_string_free(m, TRUE);
1168	g_free(h);
1169	g_free(s);
1170	g_free(i);
1171	g_free(b);
1172	g_free(a);
1173	g_free(p);
1174	g_date_time_unref(ad);
1175	g_date_time_unref(bd);
1176}
1177
1178void
1179run_user_scripts(WebKitWebView *web_view)
1180{
1181	gchar                  *c = NULL,           /* file content */
1182	                       *p = NULL,           /* path (file) */
1183	                       *b = NULL;           /* base directory (scripts) */
1184	const gchar            *e = NULL;           /* directory file entry */
1185	GDir                   *s = NULL;           /* user script directory */
1186
1187	b = g_build_filename(g_get_user_config_dir(), __NAME__, CFG_S(UsDir), NULL);
1188	if ((s = g_dir_open(b, 0, NULL)) == NULL) {
1189		g_free(b);
1190		return;
1191	}
1192
1193	while ((e = g_dir_read_name(s)) != NULL) {
1194		if (g_str_has_suffix((p = g_build_filename(b, e, NULL)), ".js")
1195		    && g_file_get_contents(p, &c, NULL, NULL)) {
1196			webkit_web_view_evaluate_javascript(
1197			    web_view, c, -1, NULL, NULL, NULL, NULL, NULL
1198			);
1199			g_free(c);
1200		}
1201		g_free(p);
1202	}
1203
1204	g_dir_close(s);
1205	g_free(b);
1206}
1207
1208void
1209save_history(const gchar *t)
1210{
1211	FILE                   *fp;
1212
1213	if (CFG_S(HistoryFile) == NULL)
1214		return;
1215
1216	if ((fp = fopen(CFG_S(HistoryFile), "a")) != NULL) {
1217		fprintf(fp, "%s\n", t);
1218		fclose(fp);
1219	} else {
1220		perror(__NAME__": Error opening history file");
1221	}
1222}
1223
1224void
1225save_state(void)
1226{
1227	GList                  *c;                  /* child (notebook page) */
1228	int                     x,                  /* state indicators */
1229	                        i,
1230	                        l;                  /* length (number of tabs) */
1231	FILE                   *fp;
1232	WebKitWebView          *wv;
1233	WebKitSettings         *s;
1234	gchar                  *u = NULL;
1235
1236	/* only save state for ipc host, when state isn't locked for loading */
1237	if (CFG_S(StateFile) == NULL || gl.ipc != IPC_HOST || gl.state_lock)
1238		return;
1239
1240	if ((fp = fopen(CFG_S(StateFile), "w")) == NULL) {
1241		perror(__NAME__": Error opening state file");
1242		return;
1243	}
1244
1245	/* save uri from each tab into state file */
1246	for (i = 0, l = gtk_notebook_get_n_pages(GTK_NOTEBOOK(mw.nb)); i < l; i++) {
1247		c = gtk_container_get_children(
1248		    GTK_CONTAINER(gtk_notebook_get_nth_page(GTK_NOTEBOOK(mw.nb), i))
1249		);
1250		if (strcmp(gtk_widget_get_name((c->data)), "WebKitWebView"))
1251			continue;
1252		wv = WEBKIT_WEB_VIEW(c->data);
1253		s = webkit_web_view_get_settings(wv);
1254		x = 0;
1255		if (gtk_notebook_get_current_page(GTK_NOTEBOOK(mw.nb)) == i)
1256			x |= STATE_CURRENT;
1257		if (webkit_settings_get_enable_javascript_markup(s))
1258			x |= STATE_JAVASCRIPT;
1259		fprintf(fp, "%d:%s\n", x, (u = get_uri((GtkWidget*)wv)));
1260		g_free(u);
1261	}
1262
1263	fclose(fp);
1264}
1265
1266void
1267search(struct Client                           *c,
1268       enum inpage_search_type                  type)
1269{
1270	if (gl.search_text == NULL)
1271		return;
1272
1273	switch (type) {
1274	case IPS_INIT:
1275		webkit_find_controller_search(
1276		    webkit_web_view_get_find_controller(WEBKIT_WEB_VIEW(c->wv)),
1277		    gl.search_text,
1278		    WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE
1279		    | WEBKIT_FIND_OPTIONS_WRAP_AROUND,
1280		    G_MAXUINT
1281		);
1282		break;
1283	case IPS_FORWARD:
1284		webkit_find_controller_search_next(
1285		    webkit_web_view_get_find_controller(WEBKIT_WEB_VIEW(c->wv))
1286		);
1287		break;
1288	case IPS_BACKWARD:
1289		webkit_find_controller_search_previous(
1290		    webkit_web_view_get_find_controller(WEBKIT_WEB_VIEW(c->wv))
1291		);
1292		break;
1293	}
1294}
1295
1296void
1297selection_search(struct Client *c)
1298{
1299	webkit_web_view_evaluate_javascript(
1300	    WEBKIT_WEB_VIEW(c->wv),
1301	    "window.getSelection().toString();",
1302	    -1,
1303	    NULL,
1304	    NULL,
1305	    NULL,
1306	    cb_selection_search_finished,
1307	    c
1308	);
1309}
1310
1311void
1312selection_search_finished(struct Client        *c,
1313                          GAsyncResult         *result)
1314{
1315	JSCValue               *v = NULL;
1316	gchar                  *s = NULL,
1317	                       *u = NULL;
1318
1319	if ((v = webkit_web_view_evaluate_javascript_finish(
1320	    WEBKIT_WEB_VIEW(c->wv), result, NULL
1321	    )) == NULL)
1322		return;
1323
1324	if (jsc_value_is_string(v)
1325	    && strlen(s = jsc_value_to_string(v))
1326	    && !jsc_context_get_exception(jsc_value_get_context(v))
1327	    && (u = resolve_uri(s)) != NULL)
1328		client_create(u, NULL, TRUE, FALSE);
1329
1330	g_free(u);
1331	g_free(s);
1332}
1333
1334void
1335set_default_web_context(void)
1336{
1337	gchar                  *p = NULL;           /* web extensions path */
1338	WebKitWebContext       *c;                  /* web context */
1339
1340	/* no context needed for clients */
1341	if (gl.ipc == IPC_CLIENT)
1342		return;
1343
1344	c = webkit_web_context_get_default();
1345
1346	p = g_build_filename(g_get_user_config_dir(), __NAME__, CFG_S(WeDir), NULL);
1347#if GTK_CHECK_VERSION(3, 98, 0) /* seems to be fixed in 3.98.0, we'll see */
1348	webkit_web_context_set_sandbox_enabled(c, TRUE);
1349	webkit_web_context_add_path_to_sandbox(c, p, TRUE);
1350#else
1351	webkit_web_context_set_web_extensions_directory(c, p);
1352#endif
1353
1354	CB(c, "download-started", cb_download_start, NULL);
1355	webkit_web_context_set_favicon_database_directory(c, NULL);
1356	webkit_web_context_register_uri_scheme(
1357	    c, ABOUT_SCHEME, (WebKitURISchemeRequestCallback)about_scheme_request,
1358	    NULL, NULL
1359	);
1360
1361	g_free(p);
1362}
1363
1364void
1365set_hover_uri(struct Client                    *c,
1366              WebKitHitTestResult              *hit_test_result)
1367{
1368	const char             *t;                  /* entry text holder */
1369	gchar                  *u = NULL;           /* uri text holder */
1370
1371	g_free(c->hover_uri);
1372
1373	/* only display hovered links */
1374	if (webkit_hit_test_result_context_is_link(hit_test_result)) {
1375		t = webkit_hit_test_result_get_link_uri(hit_test_result);
1376		c->hover_uri = g_strdup(t);
1377	} else {
1378		u = get_uri(c->wv);
1379		c->hover_uri = NULL;
1380	}
1381
1382	if (!gtk_widget_is_focus(c->entry))
1383		gtk_entry_set_text(GTK_ENTRY(c->entry), u ? u : t);
1384
1385	g_free(u);
1386}
1387
1388void
1389set_javascript_policy(struct Client            *c,
1390                      enum javascript_policy    policy)
1391{
1392	webkit_settings_set_enable_javascript_markup(
1393	    c->settings,
1394	    policy == JSP_TOGGLE
1395	        ? !webkit_settings_get_enable_javascript_markup(c->settings)
1396	        : policy
1397	);
1398	gtk_entry_set_icon_from_icon_name(
1399	    GTK_ENTRY(c->entry),
1400	    GTK_ENTRY_ICON_SECONDARY,
1401	    webkit_settings_get_enable_javascript_markup(c->settings)
1402	        ? ICON_JS_ON
1403	        : ICON_JS_OFF
1404	);
1405	webkit_web_view_reload_bypass_cache(WEBKIT_WEB_VIEW(c->wv));
1406	save_state();
1407}
1408
1409void
1410set_window_title(gint i)
1411{
1412	GtkWidget              *c;                  /* notebook child/page */
1413
1414	if ((c = gtk_notebook_get_nth_page(GTK_NOTEBOOK(mw.nb), i)) == NULL)
1415		return;
1416
1417	gtk_window_set_title(GTK_WINDOW(mw.win),
1418	    gtk_label_get_text(
1419	        GTK_LABEL((GtkWidget *)g_object_get_data(
1420	            G_OBJECT(gtk_notebook_get_tab_label(GTK_NOTEBOOK(mw.nb), c)),
1421	            __NAME__"-tab-label"
1422	        ))
1423	    )
1424	);
1425}
1426
1427void
1428show_web_view(struct Client *c)
1429{
1430	gint                    i;
1431
1432	gtk_widget_show_all(mw.win);
1433
1434	if (c->focus_new_tab) {
1435		if ((i = gtk_notebook_page_num(GTK_NOTEBOOK(mw.nb), c->vbx)) != -1)
1436			gtk_notebook_set_current_page(GTK_NOTEBOOK(mw.nb), i);
1437		gtk_widget_grab_focus(c->wv);
1438	}
1439}
1440
1441void
1442toggle_inspector(struct Client *c)
1443{
1444	WebKitWebInspector     *i;
1445
1446	i = webkit_web_view_get_inspector(WEBKIT_WEB_VIEW(c->wv));
1447
1448	/* assumes that the inspector has not been detached by the user */
1449	if (webkit_web_inspector_is_attached(i))
1450		webkit_web_inspector_close(i);
1451	else
1452		webkit_web_inspector_show(i);
1453}
1454
1455void
1456toggle_tls_error_policy(struct Client *c)
1457{
1458	webkit_website_data_manager_set_tls_errors_policy(
1459	    webkit_web_view_get_website_data_manager(WEBKIT_WEB_VIEW(c->wv)),
1460	    !webkit_website_data_manager_get_tls_errors_policy(
1461	        webkit_web_view_get_website_data_manager(WEBKIT_WEB_VIEW(c->wv))
1462	    )
1463	);
1464	webkit_web_view_reload_bypass_cache(WEBKIT_WEB_VIEW(c->wv));
1465}
1466
1467gchar *
1468resolve_uri(const gchar *t)
1469{
1470	gchar                  *u = NULL,           /* uri to return */
1471	                       *l = NULL,           /* temporary string */
1472	                       *e = NULL;           /* uri encoded string */
1473	const gchar            *s = NULL;
1474	wordexp_t               x;                  /* wordexp struct */
1475	int                     r = -1;             /* result holder for wordexp */
1476
1477	/* cannot expand string in file scheme, so try without scheme */
1478	if (!strncmp("file://", t, 7) && (t[7] == '~' || t[7] == '$'))
1479		return resolve_uri(t + 7);
1480
1481	/* use internal about page so that about: prefix is ignored by WebKit */
1482	if (!strncmp(t, "about:", 6) && strcmp(t, "about:blank"))
1483		u = g_strdup_printf(ABOUT_SCHEME":%s", t + 6);
1484	/* check if valid scheme, and if so just create a copy of the text */
1485	else if ((s = g_uri_peek_scheme(t)) && g_strv_contains(CFG_L(UriSchemes), s))
1486		u = g_strdup(t);
1487	/* if no match, then test xdg schemes (schemes that are redirected) */
1488	else if (s && CFG_L(XdgSchemes) && g_strv_contains(CFG_L(XdgSchemes), s))
1489		xdg_open(s, t);
1490	/* if path is local, use the file scheme, else try to see if the string is
1491	 * expandable and is a local path */
1492	else if ((l = realpath(t, NULL)) != NULL || ((r = wordexp(t, &x, 0)) == 0
1493	          && (l = resolve_uri_words(x.we_wordc, x.we_wordv)) != NULL))
1494		u = g_strdup_printf("file://%s", l);
1495	/* else, check if the text can be interpreted as a valid https uri; it's
1496	 * not enough to check if uri is valid - check for period and no spaces */
1497	else if (strchr(t, '.') && !strchr(t, ' ')
1498	         && g_uri_is_valid((l = g_strdup_printf("https://%s", t)), 0, NULL))
1499		u = g_strdup(l);
1500	/* fallback to web search, using a specified search engine */
1501	else
1502		u = g_strdup_printf(
1503		    CFG_S(SearchEngineUriFormat),
1504		    (e = g_uri_escape_string(t, NULL, FALSE))
1505		);
1506
1507	if (r == 0 || r == WRDE_NOSPACE) /* free on success (r == 0) and NOSPACE */
1508		wordfree(&x);
1509	g_free(e);
1510	g_free(l);
1511	return u; /* return a pointer that the caller is responsible for freeing */
1512}
1513
1514char *
1515resolve_uri_words(int c, char **w)
1516{
1517	gchar                  *d = NULL,           /* uri decoded string */
1518	                       *p = NULL,           /* path to return */
1519	                       *s = NULL;           /* concatenated string */
1520	size_t                  l,                  /* arbitrary length holder */
1521	                        wl[c];              /* word length */
1522	int                     i;
1523
1524	for (i = 0, l = 0; i < c; l += (wl[i] = strlen(w[i])) + 1, i++);
1525
1526	if (!(s = (char *)malloc(l * sizeof(char))))
1527		die(__NAME__ ": fatal: malloc failed\n");
1528
1529	/* concatenate, or join, words into a space separated string */
1530	for (i = 0, l = 0; i < c; l += wl[i], i++) {
1531		memcpy(s + l, w[i], wl[i]);
1532		memcpy(s + l++ + wl[i], i == c - 1 ? "\0" : " ", 1);
1533	}
1534
1535	p = realpath((d = g_uri_unescape_string(s, NULL)), NULL);
1536
1537	g_free(d);
1538	g_free(s);
1539	return p; /* return a pointer that the caller is responsible for freeing */
1540}
1541
1542void
1543update_download_button(WebKitDownload          *d,
1544                       GtkButton               *btn,
1545                       gboolean                 done)
1546{
1547	gdouble                 p,                  /* percent completed */
1548	                        l;                  /* response length (bytes) */
1549	gchar                  *t = NULL,           /* text holder*/
1550	                       *f = NULL,           /* file name */
1551	                       *b = NULL;           /* base name */
1552
1553	if ((f = g_filename_from_uri(webkit_download_get_destination(d), NULL, NULL)
1554	    ) == NULL)
1555		return; /* should never happen, but just in case... */
1556
1557	p = webkit_download_get_estimated_progress(d);
1558	p = (p > 1 ? 1 : p < 0 ? 0 : p) * 100;
1559	l = webkit_uri_response_get_content_length(webkit_download_get_response(d));
1560	b = g_path_get_basename(f);
1561
1562	gtk_button_set_label(btn, !done /* set label based on progress */
1563		? (t = g_strdup_printf(" %s (%.0f%% of %.1f MB)", b, p, l / 1e6))
1564		: (t = g_strdup_printf(" %s", b))
1565	);
1566
1567	g_free(t);
1568	g_free(f);
1569	g_free(b);
1570}
1571
1572void
1573update_favicon(struct Client *c)
1574{
1575	cairo_surface_t        *f;  /* favicon */
1576	GdkPixbuf              *b,  /* pix buffer */
1577	                       *s;  /* pix buffer (scaled) */
1578
1579	/* set fallback favicon */
1580	gtk_image_set_from_icon_name(
1581	    GTK_IMAGE(c->tab_icon), ICON_GLOBE, GTK_ICON_SIZE_SMALL_TOOLBAR
1582	);
1583
1584	if ((f = webkit_web_view_get_favicon(WEBKIT_WEB_VIEW(c->wv))) == NULL)
1585		return;
1586
1587	if ((b = gdk_pixbuf_get_from_surface(f, 0, 0,
1588	    cairo_image_surface_get_width(f),
1589	    cairo_image_surface_get_height(f))) == NULL)
1590		return;
1591
1592	s = gdk_pixbuf_scale_simple(b,
1593	    16 * gtk_widget_get_scale_factor(c->tab_icon),
1594	    16 * gtk_widget_get_scale_factor(c->tab_icon),
1595	    GDK_INTERP_BILINEAR
1596	);
1597	gtk_image_set_from_pixbuf(GTK_IMAGE(c->tab_icon), s);
1598
1599	g_object_unref(s);
1600	g_object_unref(b);
1601}
1602
1603void
1604update_load_progress(struct Client *c)
1605{
1606	gdouble                 p;
1607
1608	p = webkit_web_view_get_estimated_load_progress(WEBKIT_WEB_VIEW(c->wv));
1609	gtk_entry_set_progress_fraction(GTK_ENTRY(c->entry), (p == 1 ? 0 : p));
1610}
1611
1612void
1613update_title(struct Client *c)
1614{
1615	const gchar            *t;
1616	gchar                  *m = NULL,
1617	                       *u;
1618
1619	u = get_uri(c->wv);
1620	t = webkit_web_view_get_title(WEBKIT_WEB_VIEW(c->wv));
1621
1622	/* title priority: title, url, __NAME__ */
1623	t = t == NULL || t[0] == 0 ? (u == NULL || u[0] == 0 ? __NAME__ : u) : t;
1624
1625	/* set label markup and certificate icon based on tls status */
1626	if (c->failed_crt
1627	    || (c->https && c->tls_error != G_TLS_CERTIFICATE_NO_FLAGS)) {
1628		m = g_markup_printf_escaped(CFG_S(BadTlsTabFormat), t);
1629		gtk_entry_set_icon_from_icon_name(
1630		    GTK_ENTRY(c->entry), GTK_ENTRY_ICON_PRIMARY, ICON_BAD_TLS
1631		);
1632	} else {
1633		m = g_markup_printf_escaped(CFG_S(NormalTabFormat), t);
1634		if (c->https)
1635			gtk_entry_set_icon_from_icon_name(
1636			    GTK_ENTRY(c->entry), GTK_ENTRY_ICON_PRIMARY, ICON_TLS
1637			);
1638		else
1639			gtk_entry_set_icon_from_icon_name(
1640			    GTK_ENTRY(c->entry), GTK_ENTRY_ICON_PRIMARY, NULL
1641			);
1642	}
1643
1644	gtk_label_set_markup(GTK_LABEL(c->tab_label), m);
1645	gtk_widget_set_tooltip_text(c->tab_label, t);
1646	set_window_title(gtk_notebook_get_current_page(GTK_NOTEBOOK(mw.nb)));
1647	g_free(m);
1648	g_free(u);
1649}
1650
1651void
1652uri_changed(struct Client *c)
1653{
1654	gchar                  *t = NULL;
1655
1656	/* make sure to not overwrite the "WEB PROCESS CRASHED" message */
1657	if ((t = get_uri(c->wv)) != NULL && strlen(t) > 0) {
1658		gtk_entry_set_text(GTK_ENTRY(c->entry), t);
1659		save_history(t);
1660		save_state();
1661	}
1662
1663	g_free(t);
1664}
1665
1666void
1667web_view_crashed(struct Client *c)
1668{
1669	gchar                  *t = NULL,
1670	                       *u = NULL;
1671
1672	gtk_entry_set_text(
1673	    GTK_ENTRY(c->entry),
1674	    (t = g_strdup_printf("WEB PROCESS CRASHED: %s", (u = get_uri(c->wv))))
1675	);
1676	g_free(t);
1677	g_free(u);
1678}
1679
1680void
1681xdg_open(const gchar                           *s,
1682         const gchar                           *t)
1683{
1684	char                    b[URI_MAX];         /* command line buffer */
1685
1686	/* make sure to send the scheme the way it was matched */
1687	sprintf(b, "%s %s%s &", XDG_OPEN, s, t+strlen(s));
1688	system(b);
1689}
1690
1691int
1692main(int argc, char **argv)
1693{
1694	int                     opt,
1695	                        i;
1696
1697	gtk_init(&argc, &argv);
1698
1699	/* load default configuration before reading command-line arguments */
1700	load_configuration();
1701
1702	while ((opt = getopt(argc, argv, "I")) != -1)
1703		switch (opt) {
1704		case 'I':
1705			gl.ipc = IPC_NONE;
1706			break;
1707		default:
1708			die("Usage: " __NAME__ " [-I [-]] [URI ...] [FILE ...]\n");
1709		}
1710
1711	ipc_setup();
1712	set_default_web_context();
1713	main_window_setup();
1714	load_state();
1715
1716	/* load a default home uri if no clients and no arguments exist  */
1717	if (optind >= argc && gl.clients == 0)
1718		client_create(CFG_S(HomeUri), NULL, TRUE, TRUE);
1719	/* load stdin if first argument is '-' */
1720	if (optind < argc && !strcmp(argv[optind], "-"))
1721		load_stdin();
1722	/* load remaining command line arguments as uris into new clients */
1723	else if (optind < argc)
1724		for (i = optind; i < argc; i++)
1725			client_create(argv[i], NULL, TRUE, TRUE);
1726
1727	/* don't load gtk for ipc clients, they have done their bit, and make sure
1728	 * there are at least one client to view */
1729	if (gl.ipc != IPC_CLIENT && gl.clients > 0)
1730		gtk_main();
1731
1732	exit(EXIT_SUCCESS);
1733}