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