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