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