Style inquisition plus blocking of the */favicon.ico request. Thanks Peter A.
[surf.git] / surf.c
1 /* See LICENSE file for copyright and license details.
2  *
3  * To understand surf, start reading main().
4  */
5 #include <signal.h>
6 #include <X11/X.h>
7 #include <X11/Xatom.h>
8 #include <gtk/gtk.h>
9 #include <gdk/gdkx.h>
10 #include <gdk/gdk.h>
11 #include <gdk/gdkkeysyms.h>
12 #include <string.h>
13 #include <sys/types.h>
14 #include <sys/wait.h>
15 #include <unistd.h>
16 #include <limits.h>
17 #include <stdlib.h>
18 #include <stdio.h>
19 #include <webkit/webkit.h>
20 #include <glib/gstdio.h>
21 #include <JavaScriptCore/JavaScript.h>
22 #include <sys/file.h>
23
24 #include "arg.h"
25
26 char *argv0;
27
28 #define LENGTH(x)               (sizeof x / sizeof x[0])
29 #define COOKIEJAR_TYPE          (cookiejar_get_type ())
30 #define COOKIEJAR(obj)          (G_TYPE_CHECK_INSTANCE_CAST ((obj), COOKIEJAR_TYPE, CookieJar))
31
32 enum { AtomFind, AtomGo, AtomUri, AtomLast };
33
34 typedef union Arg Arg;
35 union Arg {
36         gboolean b;
37         gint i;
38         const void *v;
39 };
40
41 typedef struct Client {
42         GtkWidget *win, *scroll, *vbox, *indicator;
43         WebKitWebView *view;
44         char *title, *linkhover;
45         const char *uri, *needle;
46         gint progress;
47         gboolean sslfailed;
48         struct Client *next;
49         gboolean zoomed, fullscreen;
50 } Client;
51
52 typedef struct {
53         char *label;
54         void (*func)(Client *c, const Arg *arg);
55         const Arg arg;
56 } Item;
57
58 typedef struct {
59         guint mod;
60         guint keyval;
61         void (*func)(Client *c, const Arg *arg);
62         const Arg arg;
63 } Key;
64
65 typedef struct {
66         SoupCookieJarText parent_instance;
67         int lock;
68 } CookieJar;
69
70 typedef struct {
71         SoupCookieJarTextClass parent_class;
72 } CookieJarClass;
73
74 G_DEFINE_TYPE(CookieJar, cookiejar, SOUP_TYPE_COOKIE_JAR_TEXT)
75
76 static Display *dpy;
77 static Atom atoms[AtomLast];
78 static Client *clients = NULL;
79 static GdkNativeWindow embed = 0;
80 static gboolean showxid = FALSE;
81 static char winid[64];
82 static gboolean loadimages = 1, enableplugins = 1, enablescripts = 1,
83                 usingproxy = 0;
84 static char togglestat[5];
85
86 static void beforerequest(WebKitWebView *w, WebKitWebFrame *f,
87                 WebKitWebResource *r, WebKitNetworkRequest *req,
88                 WebKitNetworkResponse *resp, gpointer d);
89 static char *buildpath(const char *path);
90 static gboolean buttonrelease(WebKitWebView *web, GdkEventButton *e,
91                 GList *gl);
92 static void cleanup(void);
93 static void clipboard(Client *c, const Arg *arg);
94 static void cookiejar_changed(SoupCookieJar *self, SoupCookie *old_cookie,
95                 SoupCookie *new_cookie);
96 static void cookiejar_finalize(GObject *self);
97 static SoupCookieJar *cookiejar_new(const char *filename, gboolean read_only);
98 static void cookiejar_set_property(GObject *self, guint prop_id,
99                 const GValue *value, GParamSpec *pspec);
100 static char *copystr(char **str, const char *src);
101 static WebKitWebView *createwindow(WebKitWebView *v, WebKitWebFrame *f,
102                 Client *c);
103 static gboolean decidedownload(WebKitWebView *v, WebKitWebFrame *f,
104                 WebKitNetworkRequest *r, gchar *m,  WebKitWebPolicyDecision *p,
105                 Client *c);
106 static gboolean decidewindow(WebKitWebView *v, WebKitWebFrame *f,
107                 WebKitNetworkRequest *r, WebKitWebNavigationAction *n,
108                 WebKitWebPolicyDecision *p, Client *c);
109 static void destroyclient(Client *c);
110 static void destroywin(GtkWidget* w, Client *c);
111 static void die(char *str);
112 static void drawindicator(Client *c);
113 static gboolean exposeindicator(GtkWidget *w, GdkEventExpose *e, Client *c);
114 static void find(Client *c, const Arg *arg);
115 static void fullscreen(Client *c, const Arg *arg);
116 static const char *getatom(Client *c, int a);
117 static char *geturi(Client *c);
118 static gboolean initdownload(WebKitWebView *v, WebKitDownload *o, Client *c);
119 static gboolean keypress(GtkWidget *w, GdkEventKey *ev, Client *c);
120 static void linkhover(WebKitWebView *v, const char* t, const char* l,
121                 Client *c);
122 static void loadstatuschange(WebKitWebView *view, GParamSpec *pspec,
123                 Client *c);
124 static void loaduri(Client *c, const Arg *arg);
125 static void navigate(Client *c, const Arg *arg);
126 static Client *newclient(void);
127 static void newwindow(Client *c, const Arg *arg, gboolean noembed);
128 static void pasteuri(GtkClipboard *clipboard, const char *text, gpointer d);
129 static void populatepopup(WebKitWebView *web, GtkMenu *menu, Client *c);
130 static void popupactivate(GtkMenuItem *menu, Client *);
131 static void print(Client *c, const Arg *arg);
132 static GdkFilterReturn processx(GdkXEvent *xevent, GdkEvent *event,
133                 gpointer d);
134 static void progresschange(WebKitWebView *view, GParamSpec *pspec, Client *c);
135 static void reload(Client *c, const Arg *arg);
136 static void scroll_h(Client *c, const Arg *arg);
137 static void scroll_v(Client *c, const Arg *arg);
138 static void scroll(GtkAdjustment *a, const Arg *arg);
139 static void setatom(Client *c, int a, const char *v);
140 static void setup(void);
141 static void sigchld(int unused);
142 static void source(Client *c, const Arg *arg);
143 static void spawn(Client *c, const Arg *arg);
144 static void eval(Client *c, const Arg *arg);
145 static void stop(Client *c, const Arg *arg);
146 static void titlechange(WebKitWebView *v, WebKitWebFrame *frame,
147                 const char *title, Client *c);
148 static void toggle(Client *c, const Arg *arg);
149 static void gettogglestat(Client *c);
150 static void update(Client *c);
151 static void updatewinid(Client *c);
152 static void usage(void);
153 static void windowobjectcleared(GtkWidget *w, WebKitWebFrame *frame,
154                 JSContextRef js, JSObjectRef win, Client *c);
155 static void zoom(Client *c, const Arg *arg);
156
157 /* configuration, allows nested code to access above variables */
158 #include "config.h"
159
160 static void
161 beforerequest(WebKitWebView *w, WebKitWebFrame *f, WebKitWebResource *r,
162                 WebKitNetworkRequest *req, WebKitNetworkResponse *resp,
163                 gpointer d) {
164         const gchar *uri = webkit_network_request_get_uri(req);
165         if(g_str_has_suffix(uri, "/favicon.ico"))
166                 webkit_network_request_set_uri(req, "about:blank");
167 }
168
169 static char *
170 buildpath(const char *path) {
171         char *apath, *p;
172         FILE *f;
173
174         /* creating directory */
175         if(path[0] == '/') {
176                 apath = g_strdup(path);
177         } else if(path[0] == '~') {
178                 if(path[1] == '/') {
179                         apath = g_strconcat(g_get_home_dir(), &path[1], NULL);
180                 } else {
181                         apath = g_strconcat(g_get_home_dir(), "/",
182                                         &path[1], NULL);
183                 }
184         } else {
185                 apath = g_strconcat(g_get_current_dir(), "/", path, NULL);
186         }
187
188         if((p = strrchr(apath, '/'))) {
189                 *p = '\0';
190                 g_mkdir_with_parents(apath, 0700);
191                 g_chmod(apath, 0700); /* in case it existed */
192                 *p = '/';
193         }
194         /* creating file (gives error when apath ends with "/") */
195         if((f = fopen(apath, "a"))) {
196                 g_chmod(apath, 0600); /* always */
197                 fclose(f);
198         }
199
200         return apath;
201 }
202
203 static gboolean
204 buttonrelease(WebKitWebView *web, GdkEventButton *e, GList *gl) {
205         WebKitHitTestResultContext context;
206         WebKitHitTestResult *result = webkit_web_view_get_hit_test_result(web,
207                         e);
208         Arg arg;
209
210         g_object_get(result, "context", &context, NULL);
211         if(context & WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK) {
212                 if(e->button == 2) {
213                         g_object_get(result, "link-uri", &arg.v, NULL);
214                         newwindow(NULL, &arg, e->state & GDK_CONTROL_MASK);
215                         return true;
216                 }
217         }
218         return false;
219 }
220
221 static void
222 cleanup(void) {
223         while(clients)
224                 destroyclient(clients);
225         g_free(cookiefile);
226         g_free(scriptfile);
227         g_free(stylefile);
228 }
229
230 static void
231 cookiejar_changed(SoupCookieJar *self, SoupCookie *old_cookie,
232                 SoupCookie *new_cookie) {
233         flock(COOKIEJAR(self)->lock, LOCK_EX);
234         if(new_cookie && !new_cookie->expires && sessiontime) {
235                 soup_cookie_set_expires(new_cookie,
236                                 soup_date_new_from_now(sessiontime));
237         }
238         SOUP_COOKIE_JAR_CLASS(cookiejar_parent_class)->changed(self,
239                         old_cookie, new_cookie);
240         flock(COOKIEJAR(self)->lock, LOCK_UN);
241 }
242
243 static void
244 cookiejar_class_init(CookieJarClass *klass) {
245         SOUP_COOKIE_JAR_CLASS(klass)->changed = cookiejar_changed;
246         G_OBJECT_CLASS(klass)->get_property =
247                 G_OBJECT_CLASS(cookiejar_parent_class)->get_property;
248         G_OBJECT_CLASS(klass)->set_property = cookiejar_set_property;
249         G_OBJECT_CLASS(klass)->finalize = cookiejar_finalize;
250         g_object_class_override_property(G_OBJECT_CLASS(klass), 1, "filename");
251 }
252
253 static void
254 cookiejar_finalize(GObject *self) {
255         close(COOKIEJAR(self)->lock);
256         G_OBJECT_CLASS(cookiejar_parent_class)->finalize(self);
257 }
258
259 static void
260 cookiejar_init(CookieJar *self) {
261         self->lock = open(cookiefile, 0);
262 }
263
264 static SoupCookieJar *
265 cookiejar_new(const char *filename, gboolean read_only) {
266         return g_object_new(COOKIEJAR_TYPE,
267                             SOUP_COOKIE_JAR_TEXT_FILENAME, filename,
268                             SOUP_COOKIE_JAR_READ_ONLY, read_only, NULL);
269 }
270
271 static void
272 cookiejar_set_property(GObject *self, guint prop_id, const GValue *value,
273                 GParamSpec *pspec) {
274         flock(COOKIEJAR(self)->lock, LOCK_SH);
275         G_OBJECT_CLASS(cookiejar_parent_class)->set_property(self, prop_id,
276                         value, pspec);
277         flock(COOKIEJAR(self)->lock, LOCK_UN);
278 }
279
280 static void
281 evalscript(JSContextRef js, char *script, char* scriptname) {
282         JSStringRef jsscript, jsscriptname;
283         JSValueRef exception = NULL;
284
285         jsscript = JSStringCreateWithUTF8CString(script);
286         jsscriptname = JSStringCreateWithUTF8CString(scriptname);
287         JSEvaluateScript(js, jsscript, JSContextGetGlobalObject(js), jsscriptname, 0, &exception);
288         JSStringRelease(jsscript);
289         JSStringRelease(jsscriptname);
290 }
291
292 static void
293 runscript(WebKitWebFrame *frame) {
294         char *script;
295         GError *error;
296
297         if(g_file_get_contents(scriptfile, &script, NULL, &error)) {
298                 evalscript(webkit_web_frame_get_global_context(frame), script, scriptfile);
299         }
300 }
301
302 static void
303 clipboard(Client *c, const Arg *arg) {
304         gboolean paste = *(gboolean *)arg;
305
306         if(paste)
307                 gtk_clipboard_request_text(gtk_clipboard_get(GDK_SELECTION_PRIMARY), pasteuri, c);
308         else
309                 gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_PRIMARY), c->linkhover ? c->linkhover : geturi(c), -1);
310 }
311
312 static char *
313 copystr(char **str, const char *src) {
314         char *tmp;
315         tmp = g_strdup(src);
316
317         if(str && *str) {
318                 g_free(*str);
319                 *str = tmp;
320         }
321         return tmp;
322 }
323
324 static WebKitWebView *
325 createwindow(WebKitWebView  *v, WebKitWebFrame *f, Client *c) {
326         Client *n = newclient();
327         return n->view;
328 }
329
330 static gboolean
331 decidedownload(WebKitWebView *v, WebKitWebFrame *f, WebKitNetworkRequest *r,
332                 gchar *m,  WebKitWebPolicyDecision *p, Client *c) {
333         if(!webkit_web_view_can_show_mime_type(v, m)) {
334                 webkit_web_policy_decision_download(p);
335                 return TRUE;
336         }
337         return FALSE;
338 }
339
340 static gboolean
341 decidewindow(WebKitWebView *view, WebKitWebFrame *f, WebKitNetworkRequest *r,
342                 WebKitWebNavigationAction *n, WebKitWebPolicyDecision *p,
343                 Client *c) {
344         Arg arg;
345
346         if(webkit_web_navigation_action_get_reason(n) ==
347                         WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED) {
348                 webkit_web_policy_decision_ignore(p);
349                 arg.v = (void *)webkit_network_request_get_uri(r);
350                 newwindow(NULL, &arg, 0);
351                 return TRUE;
352         }
353         return FALSE;
354 }
355
356 static void
357 destroyclient(Client *c) {
358         Client *p;
359
360         webkit_web_view_stop_loading(c->view);
361         gtk_widget_destroy(c->indicator);
362         gtk_widget_destroy(GTK_WIDGET(c->view));
363         gtk_widget_destroy(c->scroll);
364         gtk_widget_destroy(c->vbox);
365         gtk_widget_destroy(c->win);
366
367         for(p = clients; p && p->next != c; p = p->next);
368         if(p) {
369                 p->next = c->next;
370         } else {
371                 clients = c->next;
372         }
373         free(c);
374         if(clients == NULL)
375                 gtk_main_quit();
376 }
377
378 static void
379 destroywin(GtkWidget* w, Client *c) {
380         destroyclient(c);
381 }
382
383 static void
384 die(char *str) {
385         fputs(str, stderr);
386         exit(EXIT_FAILURE);
387 }
388
389 static void
390 drawindicator(Client *c) {
391         gint width;
392         const char *uri;
393         char *colorname;
394         GtkWidget *w;
395         GdkGC *gc;
396         GdkColor fg;
397
398         uri = geturi(c);
399         w = c->indicator;
400         width = c->progress * w->allocation.width / 100;
401         gc = gdk_gc_new(w->window);
402         if(strstr(uri, "https://") == uri) {
403                 if(usingproxy) {
404                         colorname = c->sslfailed? progress_proxy_untrust :
405                                 progress_proxy_trust;
406                 } else {
407                         colorname = c->sslfailed? progress_untrust :
408                                 progress_trust;
409                 }
410         } else {
411                 if(usingproxy) {
412                         colorname = progress_proxy;
413                 } else {
414                         colorname = progress;
415                 }
416         }
417
418         gdk_color_parse(colorname, &fg);
419         gdk_gc_set_rgb_fg_color(gc, &fg);
420         gdk_draw_rectangle(w->window,
421                         w->style->bg_gc[GTK_WIDGET_STATE(w)],
422                         TRUE, 0, 0, w->allocation.width, w->allocation.height);
423         gdk_draw_rectangle(w->window, gc, TRUE, 0, 0, width,
424                         w->allocation.height);
425         g_object_unref(gc);
426 }
427
428 static gboolean
429 exposeindicator(GtkWidget *w, GdkEventExpose *e, Client *c) {
430         drawindicator(c);
431         return TRUE;
432 }
433
434 static void
435 find(Client *c, const Arg *arg) {
436         const char *s;
437
438         s = getatom(c, AtomFind);
439         gboolean forward = *(gboolean *)arg;
440         webkit_web_view_search_text(c->view, s, FALSE, forward, TRUE);
441 }
442
443 static void
444 fullscreen(Client *c, const Arg *arg) {
445         if(c->fullscreen) {
446                 gtk_window_unfullscreen(GTK_WINDOW(c->win));
447         } else {
448                 gtk_window_fullscreen(GTK_WINDOW(c->win));
449         }
450         c->fullscreen = !c->fullscreen;
451 }
452
453 static const char *
454 getatom(Client *c, int a) {
455         static char buf[BUFSIZ];
456         Atom adummy;
457         int idummy;
458         unsigned long ldummy;
459         unsigned char *p = NULL;
460
461         XGetWindowProperty(dpy, GDK_WINDOW_XID(GTK_WIDGET(c->win)->window),
462                         atoms[a], 0L, BUFSIZ, False, XA_STRING,
463                         &adummy, &idummy, &ldummy, &ldummy, &p);
464         if(p)
465                 strncpy(buf, (char *)p, LENGTH(buf)-1);
466         else
467                 buf[0] = '\0';
468         XFree(p);
469         return buf;
470 }
471
472 static char *
473 geturi(Client *c) {
474         char *uri;
475
476         if(!(uri = (char *)webkit_web_view_get_uri(c->view)))
477                 uri = "about:blank";
478         return uri;
479 }
480
481 static gboolean
482 initdownload(WebKitWebView *view, WebKitDownload *o, Client *c) {
483         Arg arg;
484
485         updatewinid(c);
486         arg = (Arg)DOWNLOAD((char *)webkit_download_get_uri(o), geturi(c));
487         spawn(c, &arg);
488         return FALSE;
489 }
490
491 static gboolean
492 keypress(GtkWidget* w, GdkEventKey *ev, Client *c) {
493         guint i;
494         gboolean processed = FALSE;
495
496         updatewinid(c);
497         for(i = 0; i < LENGTH(keys); i++) {
498                 if(gdk_keyval_to_lower(ev->keyval) == keys[i].keyval
499                                 && (ev->state & keys[i].mod) == keys[i].mod
500                                 && keys[i].func) {
501                         keys[i].func(c, &(keys[i].arg));
502                         processed = TRUE;
503                 }
504         }
505
506         return processed;
507 }
508
509 static void
510 linkhover(WebKitWebView *v, const char* t, const char* l, Client *c) {
511         if(l) {
512                 c->linkhover = copystr(&c->linkhover, l);
513         } else if(c->linkhover) {
514                 free(c->linkhover);
515                 c->linkhover = NULL;
516         }
517         update(c);
518 }
519
520 static void
521 loadstatuschange(WebKitWebView *view, GParamSpec *pspec, Client *c) {
522         WebKitWebFrame *frame;
523         WebKitWebDataSource *src;
524         WebKitNetworkRequest *request;
525         SoupMessage *msg;
526         char *uri;
527
528         switch(webkit_web_view_get_load_status (c->view)) {
529         case WEBKIT_LOAD_COMMITTED:
530                 uri = geturi(c);
531                 if(strstr(uri, "https://") == uri) {
532                         frame = webkit_web_view_get_main_frame(c->view);
533                         src = webkit_web_frame_get_data_source(frame);
534                         request = webkit_web_data_source_get_request(src);
535                         msg = webkit_network_request_get_message(request);
536                         c->sslfailed = soup_message_get_flags(msg)
537                                        ^ SOUP_MESSAGE_CERTIFICATE_TRUSTED;
538                 }
539                 setatom(c, AtomUri, uri);
540                 break;
541         case WEBKIT_LOAD_FINISHED:
542                 c->progress = 100;
543                 update(c);
544                 break;
545         default:
546                 break;
547         }
548 }
549
550 static void
551 loaduri(Client *c, const Arg *arg) {
552         char *u, *rp;
553         const char *uri = (char *)arg->v;
554         Arg a = { .b = FALSE };
555
556         if(strcmp(uri, "") == 0)
557                 return;
558
559         /* In case it's a file path. */
560         if(uri[0] == '/') {
561                 rp = realpath(uri, NULL);
562                 u = g_strdup_printf("file://%s", rp);
563                 free(rp);
564         } else {
565                 u = g_strrstr(uri, "://") ? g_strdup(uri)
566                         : g_strdup_printf("http://%s", uri);
567         }
568
569         /* prevents endless loop */
570         if(c->uri && strcmp(u, c->uri) == 0) {
571                 reload(c, &a);
572         } else {
573                 webkit_web_view_load_uri(c->view, u);
574                 c->progress = 0;
575                 c->title = copystr(&c->title, u);
576                 g_free(u);
577                 update(c);
578         }
579 }
580
581 static void
582 navigate(Client *c, const Arg *arg) {
583         int steps = *(int *)arg;
584         webkit_web_view_go_back_or_forward(c->view, steps);
585 }
586
587 static Client *
588 newclient(void) {
589         Client *c;
590         WebKitWebSettings *settings;
591         WebKitWebFrame *frame;
592         GdkGeometry hints = { 1, 1 };
593         char *uri, *ua;
594
595         if(!(c = calloc(1, sizeof(Client))))
596                 die("Cannot malloc!\n");
597
598         /* Window */
599         if(embed) {
600                 c->win = gtk_plug_new(embed);
601         } else {
602                 c->win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
603
604                 /* TA:  20091214:  Despite what the GNOME docs say, the ICCCM
605                  * is always correct, so we should still call this function.
606                  * But when doing so, we *must* differentiate between a
607                  * WM_CLASS and a resource on the window.  By convention, the
608                  * window class (WM_CLASS) is capped, while the resource is in
609                  * lowercase.   Both these values come as a pair.
610                  */
611                 gtk_window_set_wmclass(GTK_WINDOW(c->win), "surf", "Surf");
612
613                 /* TA:  20091214:  And set the role here as well -- so that
614                  * sessions can pick this up.
615                  */
616                 gtk_window_set_role(GTK_WINDOW(c->win), "Surf");
617         }
618         gtk_window_set_default_size(GTK_WINDOW(c->win), 800, 600);
619         g_signal_connect(G_OBJECT(c->win),
620                         "destroy",
621                         G_CALLBACK(destroywin), c);
622         g_signal_connect(G_OBJECT(c->win),
623                         "key-press-event",
624                         G_CALLBACK(keypress), c);
625
626         /* VBox */
627         c->vbox = gtk_vbox_new(FALSE, 0);
628
629         /* Scrolled Window */
630         c->scroll = gtk_scrolled_window_new(NULL, NULL);
631         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(c->scroll),
632                         GTK_POLICY_NEVER, GTK_POLICY_NEVER);
633
634         /* Webview */
635         c->view = WEBKIT_WEB_VIEW(webkit_web_view_new());
636         g_signal_connect(G_OBJECT(c->view),
637                         "title-changed",
638                         G_CALLBACK(titlechange), c);
639         g_signal_connect(G_OBJECT(c->view),
640                         "hovering-over-link",
641                         G_CALLBACK(linkhover), c);
642         g_signal_connect(G_OBJECT(c->view),
643                         "create-web-view",
644                         G_CALLBACK(createwindow), c);
645         g_signal_connect(G_OBJECT(c->view),
646                         "new-window-policy-decision-requested",
647                         G_CALLBACK(decidewindow), c);
648         g_signal_connect(G_OBJECT(c->view),
649                         "mime-type-policy-decision-requested",
650                         G_CALLBACK(decidedownload), c);
651         g_signal_connect(G_OBJECT(c->view),
652                         "window-object-cleared",
653                         G_CALLBACK(windowobjectcleared), c);
654         g_signal_connect(G_OBJECT(c->view),
655                         "notify::load-status",
656                         G_CALLBACK(loadstatuschange), c);
657         g_signal_connect(G_OBJECT(c->view),
658                         "notify::progress",
659                         G_CALLBACK(progresschange), c);
660         g_signal_connect(G_OBJECT(c->view),
661                         "download-requested",
662                         G_CALLBACK(initdownload), c);
663         g_signal_connect(G_OBJECT(c->view),
664                         "button-release-event",
665                         G_CALLBACK(buttonrelease), c);
666         g_signal_connect(G_OBJECT(c->view),
667                         "populate-popup",
668                         G_CALLBACK(populatepopup), c);
669         g_signal_connect(G_OBJECT(c->view),
670                         "resource-request-starting",
671                         G_CALLBACK(beforerequest), c);
672
673         /* Indicator */
674         c->indicator = gtk_drawing_area_new();
675         gtk_widget_set_size_request(c->indicator, 0, indicator_thickness);
676         g_signal_connect (G_OBJECT (c->indicator), "expose_event",
677                         G_CALLBACK (exposeindicator), c);
678
679         /* Arranging */
680         gtk_container_add(GTK_CONTAINER(c->scroll), GTK_WIDGET(c->view));
681         gtk_container_add(GTK_CONTAINER(c->win), c->vbox);
682         gtk_container_add(GTK_CONTAINER(c->vbox), c->scroll);
683         gtk_container_add(GTK_CONTAINER(c->vbox), c->indicator);
684
685         /* Setup */
686         gtk_box_set_child_packing(GTK_BOX(c->vbox), c->indicator, FALSE,
687                         FALSE, 0, GTK_PACK_START);
688         gtk_box_set_child_packing(GTK_BOX(c->vbox), c->scroll, TRUE,
689                         TRUE, 0, GTK_PACK_START);
690         gtk_widget_grab_focus(GTK_WIDGET(c->view));
691         gtk_widget_show(c->vbox);
692         gtk_widget_show(c->scroll);
693         gtk_widget_show(GTK_WIDGET(c->view));
694         gtk_widget_show(c->win);
695         gtk_window_set_geometry_hints(GTK_WINDOW(c->win), NULL, &hints,
696                         GDK_HINT_MIN_SIZE);
697         gdk_window_set_events(GTK_WIDGET(c->win)->window, GDK_ALL_EVENTS_MASK);
698         gdk_window_add_filter(GTK_WIDGET(c->win)->window, processx, c);
699         webkit_web_view_set_full_content_zoom(c->view, TRUE);
700
701         frame = webkit_web_view_get_main_frame(c->view);
702         runscript(frame);
703         settings = webkit_web_view_get_settings(c->view);
704         if(!(ua = getenv("SURF_USERAGENT")))
705                 ua = useragent;
706         g_object_set(G_OBJECT(settings), "user-agent", ua, NULL);
707         uri = g_strconcat("file://", stylefile, NULL);
708         g_object_set(G_OBJECT(settings), "user-stylesheet-uri", uri, NULL);
709         g_object_set(G_OBJECT(settings), "auto-load-images", loadimages, NULL);
710         g_object_set(G_OBJECT(settings), "enable-plugins", enableplugins, NULL);
711         g_object_set(G_OBJECT(settings), "enable-scripts", enablescripts, NULL);
712         g_object_set(G_OBJECT(settings), "enable-spatial-navigation",
713                         spatialbrowsing, NULL);
714
715         g_free(uri);
716
717         setatom(c, AtomFind, "");
718         setatom(c, AtomUri, "about:blank");
719         if(hidebackground)
720                 webkit_web_view_set_transparent(c->view, TRUE);
721
722         c->title = NULL;
723         c->next = clients;
724         clients = c;
725         if(showxid) {
726                 gdk_display_sync(gtk_widget_get_display(c->win));
727                 printf("%u\n",
728                         (guint)GDK_WINDOW_XID(GTK_WIDGET(c->win)->window));
729                 fflush(NULL);
730                 if (fclose(stdout) != 0) {
731                         die("Error closing stdout");
732                 }
733         }
734         return c;
735 }
736
737 static void
738 newwindow(Client *c, const Arg *arg, gboolean noembed) {
739         guint i = 0;
740         const char *cmd[10], *uri;
741         const Arg a = { .v = (void *)cmd };
742         char tmp[64];
743
744         cmd[i++] = argv0;
745         if(embed && !noembed) {
746                 cmd[i++] = "-e";
747                 snprintf(tmp, LENGTH(tmp), "%u\n", (int)embed);
748                 cmd[i++] = tmp;
749         }
750         if(!enablescripts)
751                 cmd[i++] = "-s";
752         if(!enableplugins)
753                 cmd[i++] = "-p";
754         if(!loadimages)
755                 cmd[i++] = "-i";
756         if(showxid)
757                 cmd[i++] = "-x";
758         cmd[i++] = "--";
759         uri = arg->v ? (char *)arg->v : c->linkhover;
760         if(uri)
761                 cmd[i++] = uri;
762         cmd[i++] = NULL;
763         spawn(NULL, &a);
764 }
765
766 static void
767 populatepopup(WebKitWebView *web, GtkMenu *menu, Client *c) {
768         GList *items = gtk_container_get_children(GTK_CONTAINER(menu));
769
770         for(GList *l = items; l; l = l->next) {
771                 g_signal_connect(l->data, "activate", G_CALLBACK(popupactivate), c);
772         }
773
774         g_list_free(items);
775 }
776
777 static void
778 popupactivate(GtkMenuItem *menu, Client *c) {
779         /*
780          * context-menu-action-2000     open link
781          * context-menu-action-1        open link in window
782          * context-menu-action-2        download linked file
783          * context-menu-action-3        copy link location
784          * context-menu-action-13       reload
785          * context-menu-action-10       back
786          * context-menu-action-11       forward
787          * context-menu-action-12       stop
788          */
789
790         GtkAction *a = NULL;
791         const char *name;
792         GtkClipboard *prisel;
793
794         a = gtk_activatable_get_related_action(GTK_ACTIVATABLE(menu));
795         if(a == NULL)
796                 return;
797
798         name = gtk_action_get_name(a);
799         if(!g_strcmp0(name, "context-menu-action-3")) {
800                 prisel = gtk_clipboard_get(GDK_SELECTION_PRIMARY);
801                 gtk_clipboard_set_text(prisel, c->linkhover, -1);
802         }
803 }
804
805 static void
806 pasteuri(GtkClipboard *clipboard, const char *text, gpointer d) {
807         Arg arg = {.v = text };
808         if(text != NULL)
809                 loaduri((Client *) d, &arg);
810 }
811
812 static void
813 print(Client *c, const Arg *arg) {
814         webkit_web_frame_print(webkit_web_view_get_main_frame(c->view));
815 }
816
817 static GdkFilterReturn
818 processx(GdkXEvent *e, GdkEvent *event, gpointer d) {
819         Client *c = (Client *)d;
820         XPropertyEvent *ev;
821         Arg arg;
822
823         if(((XEvent *)e)->type == PropertyNotify) {
824                 ev = &((XEvent *)e)->xproperty;
825                 if(ev->state == PropertyNewValue) {
826                         if(ev->atom == atoms[AtomFind]) {
827                                 arg.b = TRUE;
828                                 find(c, &arg);
829                                 return GDK_FILTER_REMOVE;
830                         }
831                         else if(ev->atom == atoms[AtomGo]) {
832                                 arg.v = getatom(c, AtomGo);
833                                 loaduri(c, &arg);
834                                 return GDK_FILTER_REMOVE;
835                         }
836                 }
837         }
838         return GDK_FILTER_CONTINUE;
839 }
840
841 static void
842 progresschange(WebKitWebView *view, GParamSpec *pspec, Client *c) {
843         c->progress = webkit_web_view_get_progress(c->view) * 100;
844         update(c);
845 }
846
847 static void
848 reload(Client *c, const Arg *arg) {
849         gboolean nocache = *(gboolean *)arg;
850         if(nocache)
851                  webkit_web_view_reload_bypass_cache(c->view);
852         else
853                  webkit_web_view_reload(c->view);
854 }
855
856 static void
857 scroll_h(Client *c, const Arg *arg) {
858         scroll(gtk_scrolled_window_get_hadjustment(
859                                 GTK_SCROLLED_WINDOW(c->scroll)), arg);
860 }
861
862 static void
863 scroll_v(Client *c, const Arg *arg) {
864         scroll(gtk_scrolled_window_get_vadjustment(
865                                 GTK_SCROLLED_WINDOW(c->scroll)), arg);
866 }
867
868 static void
869 scroll(GtkAdjustment *a, const Arg *arg) {
870         gdouble v;
871
872         v = gtk_adjustment_get_value(a);
873         switch (arg->i){
874         case +10000:
875         case -10000:
876                 v += gtk_adjustment_get_page_increment(a) *
877                         (arg->i / 10000);
878                 break;
879         case +20000:
880         case -20000:
881         default:
882                 v += gtk_adjustment_get_step_increment(a) * arg->i;
883         }
884
885         v = MAX(v, 0.0);
886         v = MIN(v, gtk_adjustment_get_upper(a) -
887                         gtk_adjustment_get_page_size(a));
888         gtk_adjustment_set_value(a, v);
889 }
890
891 static void
892 setatom(Client *c, int a, const char *v) {
893         XSync(dpy, False);
894         XChangeProperty(dpy, GDK_WINDOW_XID(GTK_WIDGET(c->win)->window),
895                         atoms[a], XA_STRING, 8, PropModeReplace,
896                         (unsigned char *)v, strlen(v) + 1);
897 }
898
899 static void
900 setup(void) {
901         char *proxy;
902         char *new_proxy;
903         SoupURI *puri;
904         SoupSession *s;
905
906         /* clean up any zombies immediately */
907         sigchld(0);
908         gtk_init(NULL, NULL);
909         if (!g_thread_supported())
910                 g_thread_init(NULL);
911
912         dpy = GDK_DISPLAY();
913
914         /* atoms */
915         atoms[AtomFind] = XInternAtom(dpy, "_SURF_FIND", False);
916         atoms[AtomGo] = XInternAtom(dpy, "_SURF_GO", False);
917         atoms[AtomUri] = XInternAtom(dpy, "_SURF_URI", False);
918
919         /* dirs and files */
920         cookiefile = buildpath(cookiefile);
921         scriptfile = buildpath(scriptfile);
922         stylefile = buildpath(stylefile);
923
924         /* request handler */
925         s = webkit_get_default_session();
926
927         /* cookie jar */
928         soup_session_add_feature(s,
929                         SOUP_SESSION_FEATURE(cookiejar_new(cookiefile,
930                                         FALSE)));
931
932         /* ssl */
933         g_object_set(G_OBJECT(s), "ssl-ca-file", cafile, NULL);
934         g_object_set(G_OBJECT(s), "ssl-strict", strictssl, NULL);
935
936         /* proxy */
937         if((proxy = getenv("http_proxy")) && strcmp(proxy, "")) {
938                 new_proxy = g_strrstr(proxy, "http://") ? g_strdup(proxy) :
939                         g_strdup_printf("http://%s", proxy);
940                 puri = soup_uri_new(new_proxy);
941                 g_object_set(G_OBJECT(s), "proxy-uri", puri, NULL);
942                 soup_uri_free(puri);
943                 g_free(new_proxy);
944                 usingproxy = 1;
945         }
946 }
947
948 static void
949 sigchld(int unused) {
950         if(signal(SIGCHLD, sigchld) == SIG_ERR)
951                 die("Can't install SIGCHLD handler");
952         while(0 < waitpid(-1, NULL, WNOHANG));
953 }
954
955 static void
956 source(Client *c, const Arg *arg) {
957         Arg a = { .b = FALSE };
958         gboolean s;
959
960         s = webkit_web_view_get_view_source_mode(c->view);
961         webkit_web_view_set_view_source_mode(c->view, !s);
962         reload(c, &a);
963 }
964
965 static void
966 spawn(Client *c, const Arg *arg) {
967         if(fork() == 0) {
968                 if(dpy)
969                         close(ConnectionNumber(dpy));
970                 setsid();
971                 execvp(((char **)arg->v)[0], (char **)arg->v);
972                 fprintf(stderr, "surf: execvp %s", ((char **)arg->v)[0]);
973                 perror(" failed");
974                 exit(0);
975         }
976 }
977
978 static void
979 eval(Client *c, const Arg *arg) {
980         WebKitWebFrame *frame = webkit_web_view_get_main_frame(c->view);
981         evalscript(webkit_web_frame_get_global_context(frame),
982                         ((char **)arg->v)[0], "");
983 }
984
985 static void
986 stop(Client *c, const Arg *arg) {
987         webkit_web_view_stop_loading(c->view);
988 }
989
990 static void
991 titlechange(WebKitWebView *v, WebKitWebFrame *f, const char *t, Client *c) {
992         c->title = copystr(&c->title, t);
993         update(c);
994 }
995
996 static void
997 toggle(Client *c, const Arg *arg) {
998         WebKitWebSettings *settings;
999         char *name = (char *)arg->v;
1000         gboolean value;
1001         Arg a = { .b = FALSE };
1002
1003         settings = webkit_web_view_get_settings(c->view);
1004         g_object_get(G_OBJECT(settings), name, &value, NULL);
1005         g_object_set(G_OBJECT(settings), name, !value, NULL);
1006
1007         reload(c,&a);
1008 }
1009
1010 static void
1011 gettogglestat(Client *c){
1012         gboolean value;
1013         WebKitWebSettings *settings = webkit_web_view_get_settings(c->view);
1014
1015         g_object_get(G_OBJECT(settings), "enable-caret-browsing",
1016                         &value, NULL);
1017         togglestat[0] = value? 'C': 'c';
1018
1019         g_object_get(G_OBJECT(settings), "auto-load-images", &value, NULL);
1020         togglestat[1] = value? 'I': 'i';
1021
1022         g_object_get(G_OBJECT(settings), "enable-scripts", &value, NULL);
1023         togglestat[2] = value? 'S': 's';
1024
1025         g_object_get(G_OBJECT(settings), "enable-plugins", &value, NULL);
1026         togglestat[3] = value? 'V': 'v';
1027
1028         togglestat[4] = '\0';
1029 }
1030
1031
1032 static void
1033 update(Client *c) {
1034         char *t;
1035
1036         gettogglestat(c);
1037
1038         if(c->linkhover) {
1039                 t = g_strdup_printf("%s| %s", togglestat, c->linkhover);
1040         } else if(c->progress != 100) {
1041                 drawindicator(c);
1042                 gtk_widget_show(c->indicator);
1043                 t = g_strdup_printf("[%i%%] %s| %s", c->progress, togglestat,
1044                                 c->title);
1045         } else {
1046                 gtk_widget_hide_all(c->indicator);
1047                 t = g_strdup_printf("%s| %s", togglestat, c->title);
1048         }
1049
1050         gtk_window_set_title(GTK_WINDOW(c->win), t);
1051         g_free(t);
1052 }
1053
1054 static void
1055 updatewinid(Client *c) {
1056         snprintf(winid, LENGTH(winid), "%u",
1057                         (int)GDK_WINDOW_XID(GTK_WIDGET(c->win)->window));
1058 }
1059
1060 static void
1061 usage(void) {
1062         fputs("surf - simple browser\n", stderr);
1063         die("usage: surf [-c cookiefile] [-e xid] [-i] [-p] [-r scriptfile]"
1064                 " [-s] [-t stylefile] [-u useragent] [-v] [-x] [uri]\n");
1065 }
1066
1067 static void
1068 windowobjectcleared(GtkWidget *w, WebKitWebFrame *frame, JSContextRef js,
1069                 JSObjectRef win, Client *c) {
1070         runscript(frame);
1071 }
1072
1073 static void
1074 zoom(Client *c, const Arg *arg) {
1075         c->zoomed = TRUE;
1076         if(arg->i < 0) {
1077                 /* zoom out */
1078                 webkit_web_view_zoom_out(c->view);
1079         } else if(arg->i > 0) {
1080                 /* zoom in */
1081                 webkit_web_view_zoom_in(c->view);
1082         } else {
1083                 /* reset */
1084                 c->zoomed = FALSE;
1085                 webkit_web_view_set_zoom_level(c->view, 1.0);
1086         }
1087 }
1088
1089 int
1090 main(int argc, char *argv[]) {
1091         Arg arg;
1092
1093         memset(&arg, 0, sizeof(arg));
1094
1095         /* command line args */
1096         ARGBEGIN {
1097         case 'c':
1098                 cookiefile = EARGF(usage());
1099                 break;
1100         case 'e':
1101                 embed = strtol(EARGF(usage()), NULL, 0);
1102                 break;
1103         case 'i':
1104                 loadimages = 0;
1105                 break;
1106         case 'p':
1107                 enableplugins = 0;
1108                 break;
1109         case 'r':
1110                 scriptfile = EARGF(usage());
1111                 break;
1112         case 's':
1113                 enablescripts = 0;
1114                 break;
1115         case 't':
1116                 stylefile = EARGF(usage());
1117                 break;
1118         case 'u':
1119                 useragent = EARGF(usage());
1120                 break;
1121         case 'x':
1122                 showxid = TRUE;
1123                 break;
1124         case 'v':
1125                 die("surf-"VERSION", ©2009-2012 surf engineers, see LICENSE for details\n");
1126         default:
1127                 usage();
1128         } ARGEND;
1129         if(argc > 0)
1130                 arg.v = argv[0];
1131
1132         setup();
1133         newclient();
1134         if(arg.v)
1135                 loaduri(clients, &arg);
1136
1137         gtk_main();
1138         cleanup();
1139
1140         return EXIT_SUCCESS;
1141 }
1142