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