07c689e57fe8a2a4efcc81ed9a426a244d8f26e4
[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         }
457         else if(c->linkhover) {
458                 free(c->linkhover);
459                 c->linkhover = NULL;
460         }
461         update(c);
462 }
463
464 void
465 loadstatuschange(WebKitWebView *view, GParamSpec *pspec, Client *c) {
466         WebKitWebFrame *frame;
467         WebKitWebDataSource *src;
468         WebKitNetworkRequest *request;
469         SoupMessage *msg;
470         char *uri;
471
472         switch(webkit_web_view_get_load_status (c->view)) {
473         case WEBKIT_LOAD_COMMITTED:
474                 uri = geturi(c);
475                 if(strstr(uri, "https://") == uri) {
476                         frame = webkit_web_view_get_main_frame(c->view);
477                         src = webkit_web_frame_get_data_source(frame);
478                         request = webkit_web_data_source_get_request(src);
479                         msg = webkit_network_request_get_message(request);
480                         c->sslfailed = soup_message_get_flags(msg)
481                                        ^ SOUP_MESSAGE_CERTIFICATE_TRUSTED;
482                 }
483                 setatom(c, AtomUri, uri);
484                 break;
485         case WEBKIT_LOAD_FINISHED:
486                 c->progress = 100;
487                 update(c);
488                 break;
489         default:
490                 break;
491         }
492 }
493
494 void
495 loaduri(Client *c, const Arg *arg) {
496         char *u;
497         const char *uri = (char *)arg->v;
498         Arg a = { .b = FALSE };
499
500         if(strcmp(uri, "") == 0)
501                 return;
502         u = g_strrstr(uri, "://") ? g_strdup(uri)
503                 : g_strdup_printf("http://%s", uri);
504         /* prevents endless loop */
505         if(c->uri && strcmp(u, c->uri) == 0) {
506                 reload(c, &a);
507         }
508         else {
509                 webkit_web_view_load_uri(c->view, u);
510                 c->progress = 0;
511                 c->title = copystr(&c->title, u);
512                 g_free(u);
513                 update(c);
514         }
515 }
516
517 void
518 navigate(Client *c, const Arg *arg) {
519         int steps = *(int *)arg;
520         webkit_web_view_go_back_or_forward(c->view, steps);
521 }
522
523 Client *
524 newclient(void) {
525         Client *c;
526         WebKitWebSettings *settings;
527         WebKitWebFrame *frame;
528         GdkGeometry hints = { 1, 1 };
529         char *uri, *ua;
530
531         if(!(c = calloc(1, sizeof(Client))))
532                 die("Cannot malloc!\n");
533         /* Window */
534         if(embed) {
535                 c->win = gtk_plug_new(embed);
536         }
537         else {
538                 c->win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
539                 /* TA:  20091214:  Despite what the GNOME docs say, the ICCCM
540                  * is always correct, so we should still call this function.
541                  * But when doing so, we *must* differentiate between a
542                  * WM_CLASS and a resource on the window.  By convention, the
543                  * window class (WM_CLASS) is capped, while the resource is in
544                  * lowercase.   Both these values come as a pair.
545                  */
546                 gtk_window_set_wmclass(GTK_WINDOW(c->win), "surf", "Surf");
547
548                 /* TA:  20091214:  And set the role here as well -- so that
549                  * sessions can pick this up.
550                  */
551                 gtk_window_set_role(GTK_WINDOW(c->win), "Surf");
552         }
553         gtk_window_set_default_size(GTK_WINDOW(c->win), 800, 600);
554         g_signal_connect(G_OBJECT(c->win), "destroy", G_CALLBACK(destroywin), c);
555         g_signal_connect(G_OBJECT(c->win), "key-press-event", G_CALLBACK(keypress), c);
556
557         /* VBox */
558         c->vbox = gtk_vbox_new(FALSE, 0);
559
560         /* Scrolled Window */
561         c->scroll = gtk_scrolled_window_new(NULL, NULL);
562         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(c->scroll),
563                         GTK_POLICY_NEVER, GTK_POLICY_NEVER);
564
565         /* Webview */
566         c->view = WEBKIT_WEB_VIEW(webkit_web_view_new());
567         g_signal_connect(G_OBJECT(c->view), "title-changed", G_CALLBACK(titlechange), c);
568         g_signal_connect(G_OBJECT(c->view), "hovering-over-link", G_CALLBACK(linkhover), c);
569         g_signal_connect(G_OBJECT(c->view), "create-web-view", G_CALLBACK(createwindow), c);
570         g_signal_connect(G_OBJECT(c->view), "new-window-policy-decision-requested", G_CALLBACK(decidewindow), c);
571         g_signal_connect(G_OBJECT(c->view), "mime-type-policy-decision-requested", G_CALLBACK(decidedownload), c);
572         g_signal_connect(G_OBJECT(c->view), "window-object-cleared", G_CALLBACK(windowobjectcleared), c);
573         g_signal_connect(G_OBJECT(c->view), "notify::load-status", G_CALLBACK(loadstatuschange), c);
574         g_signal_connect(G_OBJECT(c->view), "notify::progress", G_CALLBACK(progresschange), c);
575         g_signal_connect(G_OBJECT(c->view), "download-requested", G_CALLBACK(initdownload), c);
576         g_signal_connect(G_OBJECT(c->view), "button-release-event", G_CALLBACK(buttonrelease), c);
577         g_signal_connect(G_OBJECT(c->view), "populate-popup", G_CALLBACK(populatepopup), c);
578
579         /* Indicator */
580         c->indicator = gtk_drawing_area_new();
581         gtk_widget_set_size_request(c->indicator, 0, indicator_thickness);
582         g_signal_connect (G_OBJECT (c->indicator), "expose_event",
583                         G_CALLBACK (exposeindicator), c);
584
585         /* Arranging */
586         gtk_container_add(GTK_CONTAINER(c->scroll), GTK_WIDGET(c->view));
587         gtk_container_add(GTK_CONTAINER(c->win), c->vbox);
588         gtk_container_add(GTK_CONTAINER(c->vbox), c->scroll);
589         gtk_container_add(GTK_CONTAINER(c->vbox), c->indicator);
590
591         /* Setup */
592         gtk_box_set_child_packing(GTK_BOX(c->vbox), c->indicator, FALSE, FALSE, 0, GTK_PACK_START);
593         gtk_box_set_child_packing(GTK_BOX(c->vbox), c->scroll, TRUE, TRUE, 0, GTK_PACK_START);
594         gtk_widget_grab_focus(GTK_WIDGET(c->view));
595         gtk_widget_show(c->vbox);
596         gtk_widget_show(c->scroll);
597         gtk_widget_show(GTK_WIDGET(c->view));
598         gtk_widget_show(c->win);
599         gtk_window_set_geometry_hints(GTK_WINDOW(c->win), NULL, &hints, GDK_HINT_MIN_SIZE);
600         gdk_window_set_events(GTK_WIDGET(c->win)->window, GDK_ALL_EVENTS_MASK);
601         gdk_window_add_filter(GTK_WIDGET(c->win)->window, processx, c);
602         webkit_web_view_set_full_content_zoom(c->view, TRUE);
603         frame = webkit_web_view_get_main_frame(c->view);
604         runscript(frame);
605         settings = webkit_web_view_get_settings(c->view);
606         if(!(ua = getenv("SURF_USERAGENT")))
607                 ua = useragent;
608         g_object_set(G_OBJECT(settings), "user-agent", ua, NULL);
609         uri = g_strconcat("file://", stylefile, NULL);
610         g_object_set(G_OBJECT(settings), "user-stylesheet-uri", uri, NULL);
611         g_object_set(G_OBJECT(settings), "auto-load-images", loadimage, NULL);
612         g_object_set(G_OBJECT(settings), "enable-plugins", plugin, NULL);
613         g_object_set(G_OBJECT(settings), "enable-scripts", script, NULL);
614         g_object_set(G_OBJECT(settings), "enable-spatial-navigation", SPATIAL_BROWSING, NULL);
615
616         g_free(uri);
617
618         setatom(c, AtomFind, "");
619         setatom(c, AtomUri, "about:blank");
620         if(HIDE_BACKGROUND)
621                 webkit_web_view_set_transparent(c->view, TRUE);
622
623         c->title = NULL;
624         c->next = clients;
625         clients = c;
626         if(showxid) {
627                 gdk_display_sync(gtk_widget_get_display(c->win));
628                 printf("%u\n", (guint)GDK_WINDOW_XID(GTK_WIDGET(c->win)->window));
629                 fflush(NULL);
630                 if (fclose(stdout) != 0) {
631                         die("Error closing stdout");
632                 }
633         }
634         return c;
635 }
636
637 void
638 newwindow(Client *c, const Arg *arg, gboolean noembed) {
639         guint i = 0;
640         const char *cmd[10], *uri;
641         const Arg a = { .v = (void *)cmd };
642         char tmp[64];
643
644         cmd[i++] = argv0;
645         if(embed && !noembed) {
646                 cmd[i++] = "-e";
647                 snprintf(tmp, LENGTH(tmp), "%u\n", (int)embed);
648                 cmd[i++] = tmp;
649         }
650         if(!script)
651                 cmd[i++] = "-s";
652         if(!plugin)
653                 cmd[i++] = "-p";
654         if(!loadimage)
655                 cmd[i++] = "-i";
656         if(showxid)
657                 cmd[i++] = "-x";
658         cmd[i++] = "--";
659         uri = arg->v ? (char *)arg->v : c->linkhover;
660         if(uri)
661                 cmd[i++] = uri;
662         cmd[i++] = NULL;
663         spawn(NULL, &a);
664 }
665
666 static void
667 populatepopup(WebKitWebView *web, GtkMenu *menu, Client *c) {
668         GList *items = gtk_container_get_children(GTK_CONTAINER(menu));
669
670         for(GList *l = items; l; l = l->next) {
671                 g_signal_connect(l->data, "activate", G_CALLBACK(popupactivate), c);
672         }
673
674         g_list_free(items);
675 }
676
677 static void
678 popupactivate(GtkMenuItem *menu, Client *c) {
679         /*
680          * context-menu-action-2000     open link
681          * context-menu-action-1        open link in window
682          * context-menu-action-2        download linked file
683          * context-menu-action-3        copy link location
684          * context-menu-action-13       reload
685          * context-menu-action-10       back
686          * context-menu-action-11       forward
687          * context-menu-action-12       stop
688          */
689
690         GtkAction *a = NULL;
691         const char *name;
692         GtkClipboard *prisel;
693
694         a = gtk_activatable_get_related_action(GTK_ACTIVATABLE(menu));
695         if(a == NULL)
696                 return;
697
698         name = gtk_action_get_name(a);
699         if(!g_strcmp0(name, "context-menu-action-3")) {
700                 prisel = gtk_clipboard_get(GDK_SELECTION_PRIMARY);
701                 gtk_clipboard_set_text(prisel, c->linkhover, -1);
702         }
703 }
704
705 void
706 pasteuri(GtkClipboard *clipboard, const char *text, gpointer d) {
707         Arg arg = {.v = text };
708         if(text != NULL)
709                 loaduri((Client *) d, &arg);
710 }
711
712 void
713 print(Client *c, const Arg *arg) {
714         webkit_web_frame_print(webkit_web_view_get_main_frame(c->view));
715 }
716
717 GdkFilterReturn
718 processx(GdkXEvent *e, GdkEvent *event, gpointer d) {
719         Client *c = (Client *)d;
720         XPropertyEvent *ev;
721         Arg arg;
722
723         if(((XEvent *)e)->type == PropertyNotify) {
724                 ev = &((XEvent *)e)->xproperty;
725                 if(ev->state == PropertyNewValue) {
726                         if(ev->atom == atoms[AtomFind]) {
727                                 arg.b = TRUE;
728                                 find(c, &arg);
729                                 return GDK_FILTER_REMOVE;
730                         }
731                         else if(ev->atom == atoms[AtomGo]) {
732                                 arg.v = getatom(c, AtomGo);
733                                 loaduri(c, &arg);
734                                 return GDK_FILTER_REMOVE;
735                         }
736                 }
737         }
738         return GDK_FILTER_CONTINUE;
739 }
740
741 void
742 progresschange(WebKitWebView *view, GParamSpec *pspec, Client *c) {
743         c->progress = webkit_web_view_get_progress(c->view) * 100;
744         update(c);
745 }
746
747 void
748 reload(Client *c, const Arg *arg) {
749         gboolean nocache = *(gboolean *)arg;
750         if(nocache)
751                  webkit_web_view_reload_bypass_cache(c->view);
752         else
753                  webkit_web_view_reload(c->view);
754 }
755
756 void
757 scroll_h(Client *c, const Arg *arg) {
758         scroll(gtk_scrolled_window_get_hadjustment(
759                                 GTK_SCROLLED_WINDOW(c->scroll)), arg);
760 }
761
762 void
763 scroll_v(Client *c, const Arg *arg) {
764         scroll(gtk_scrolled_window_get_vadjustment(
765                                 GTK_SCROLLED_WINDOW(c->scroll)), arg);
766 }
767
768 void
769 scroll(GtkAdjustment *a, const Arg *arg) {
770         gdouble v;
771
772         v = gtk_adjustment_get_value(a);
773         switch (arg->i){
774         case +10000:
775         case -10000:
776                 v += gtk_adjustment_get_page_increment(a) *
777                         (arg->i / 10000);
778                 break;
779         case +20000:
780         case -20000:
781         default:
782                 v += gtk_adjustment_get_step_increment(a) * arg->i;
783         }
784
785         v = MAX(v, 0.0);
786         v = MIN(v, gtk_adjustment_get_upper(a) -
787                         gtk_adjustment_get_page_size(a));
788         gtk_adjustment_set_value(a, v);
789 }
790
791 void
792 setatom(Client *c, int a, const char *v) {
793         XSync(dpy, False);
794         XChangeProperty(dpy, GDK_WINDOW_XID(GTK_WIDGET(c->win)->window), atoms[a],
795                         XA_STRING, 8, PropModeReplace, (unsigned char *)v,
796                         strlen(v) + 1);
797 }
798
799 void
800 setup(void) {
801         char *proxy;
802         char *new_proxy;
803         SoupURI *puri;
804         SoupSession *s;
805
806         /* clean up any zombies immediately */
807         sigchld(0);
808         gtk_init(NULL, NULL);
809         if (!g_thread_supported())
810                 g_thread_init(NULL);
811
812         dpy = GDK_DISPLAY();
813
814         /* atoms */
815         atoms[AtomFind] = XInternAtom(dpy, "_SURF_FIND", False);
816         atoms[AtomGo] = XInternAtom(dpy, "_SURF_GO", False);
817         atoms[AtomUri] = XInternAtom(dpy, "_SURF_URI", False);
818
819         /* dirs and files */
820         cookiefile = buildpath(cookiefile);
821         scriptfile = buildpath(scriptfile);
822         stylefile = buildpath(stylefile);
823
824         /* request handler */
825         s = webkit_get_default_session();
826
827         /* cookie jar */
828         soup_session_add_feature(s, SOUP_SESSION_FEATURE(cookiejar_new(cookiefile, FALSE)));
829
830         /* ssl */
831         g_object_set(G_OBJECT(s), "ssl-ca-file", cafile, NULL);
832         g_object_set(G_OBJECT(s), "ssl-strict", strictssl, NULL);
833
834         /* proxy */
835         if((proxy = getenv("http_proxy")) && strcmp(proxy, "")) {
836                 new_proxy = g_strrstr(proxy, "http://") ? g_strdup(proxy) :
837                         g_strdup_printf("http://%s", proxy);
838                 puri = soup_uri_new(new_proxy);
839                 g_object_set(G_OBJECT(s), "proxy-uri", puri, NULL);
840                 soup_uri_free(puri);
841                 g_free(new_proxy);
842                 using_proxy = 1;
843         }
844 }
845
846 void
847 sigchld(int unused) {
848         if(signal(SIGCHLD, sigchld) == SIG_ERR)
849                 die("Can't install SIGCHLD handler");
850         while(0 < waitpid(-1, NULL, WNOHANG));
851 }
852
853 void
854 source(Client *c, const Arg *arg) {
855         Arg a = { .b = FALSE };
856         gboolean s;
857
858         s = webkit_web_view_get_view_source_mode(c->view);
859         webkit_web_view_set_view_source_mode(c->view, !s);
860         reload(c, &a);
861 }
862
863 void
864 spawn(Client *c, const Arg *arg) {
865         if(fork() == 0) {
866                 if(dpy)
867                         close(ConnectionNumber(dpy));
868                 setsid();
869                 execvp(((char **)arg->v)[0], (char **)arg->v);
870                 fprintf(stderr, "surf: execvp %s", ((char **)arg->v)[0]);
871                 perror(" failed");
872                 exit(0);
873         }
874 }
875
876 void
877 eval(Client *c, const Arg *arg) {
878         WebKitWebFrame *frame = webkit_web_view_get_main_frame(c->view);
879         evalscript(webkit_web_frame_get_global_context(frame),
880                         ((char **)arg->v)[0], "");
881 }
882
883 void
884 stop(Client *c, const Arg *arg) {
885         webkit_web_view_stop_loading(c->view);
886 }
887
888 void
889 titlechange(WebKitWebView *v, WebKitWebFrame *f, const char *t, Client *c) {
890         c->title = copystr(&c->title, t);
891         update(c);
892 }
893
894 void
895 update(Client *c) {
896         char *t;
897
898         if(c->linkhover) {
899                 t = g_strdup(c->linkhover);
900         } else if(c->progress != 100) {
901                 drawindicator(c);
902                 gtk_widget_show(c->indicator);
903                 t = g_strdup_printf("[%i%%] %s", c->progress, c->title);
904         } else {
905                 gtk_widget_hide_all(c->indicator);
906                 t = g_strdup(c->title);
907         }
908         gtk_window_set_title(GTK_WINDOW(c->win), t);
909         g_free(t);
910 }
911
912 void
913 updatewinid(Client *c) {
914         snprintf(winid, LENGTH(winid), "%u",
915                         (int)GDK_WINDOW_XID(GTK_WIDGET(c->win)->window));
916 }
917
918 void
919 usage(void) {
920         fputs("surf - simple browser\n", stderr);
921         die("usage: surf [-c cookiefile] [-e xid] [-i] [-p] [-r scriptfile]"
922                 " [-s] [-t stylefile] [-u useragent] [-v] [-x] [uri]\n");
923 }
924
925 void
926 windowobjectcleared(GtkWidget *w, WebKitWebFrame *frame, JSContextRef js, JSObjectRef win, Client *c) {
927         runscript(frame);
928 }
929
930 void
931 zoom(Client *c, const Arg *arg) {
932         c->zoomed = TRUE;
933         if(arg->i < 0)          /* zoom out */
934                 webkit_web_view_zoom_out(c->view);
935         else if(arg->i > 0)     /* zoom in */
936                 webkit_web_view_zoom_in(c->view);
937         else {                  /* reset */
938                 c->zoomed = FALSE;
939                 webkit_web_view_set_zoom_level(c->view, 1.0);
940         }
941 }
942
943 int
944 main(int argc, char *argv[]) {
945         Arg arg;
946
947         /* command line args */
948         ARGBEGIN {
949         case 'c':
950                 cookiefile = EARGF(usage());
951                 break;
952         case 'e':
953                 embed = strtol(EARGF(usage()), NULL, 0);
954                 break;
955         case 'i':
956                 loadimage = 0;
957                 break;
958         case 'p':
959                 plugin = 0;
960                 break;
961         case 'r':
962                 scriptfile = EARGF(usage());
963                 break;
964         case 's':
965                 script = 0;
966                 break;
967         case 't':
968                 stylefile = EARGF(usage());
969                 break;
970         case 'u':
971                 useragent = EARGF(usage());
972                 break;
973         case 'x':
974                 showxid = TRUE;
975                 break;
976         case 'v':
977                 die("surf-"VERSION", ©2009-2012 surf engineers, see LICENSE for details\n");
978         default:
979                 usage();
980         } ARGEND;
981         if(argc > 0)
982                 arg.v = argv[0];
983
984         setup();
985         newclient();
986         if(arg.v)
987                 loaduri(clients, &arg);
988         gtk_main();
989         cleanup();
990
991         return EXIT_SUCCESS;
992 }
993