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