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