192e26ad278996a127bd0a62f5d2fb31ce933e98
[surf.git] / surf.c
1 /* See LICENSE file for copyright and license details.
2  *
3  * To understand surf, start reading main().
4  */
5 #include <X11/X.h>
6 #include <X11/Xatom.h>
7 #include <gtk/gtk.h>
8 #include <gdk/gdkx.h>
9 #include <gdk/gdk.h>
10 #include <gdk/gdkkeysyms.h>
11 #include <string.h>
12 #include <unistd.h>
13 #include <getopt.h>
14 #include <stdlib.h>
15 #include <stdio.h>
16 #include <webkit/webkit.h>
17 #include <glib/gstdio.h>
18
19 #define LENGTH(x) (sizeof x / sizeof x[0])
20
21 Display *dpy;
22 Atom urlprop;
23 typedef struct Client {
24         GtkWidget *win, *scroll, *vbox, *urlbar, *searchbar;
25         WebKitWebView *view;
26         WebKitDownload *download;
27         gchar *title;
28         gint progress;
29         struct Client *next;
30 } Client;
31
32 typedef struct Cookie {
33         char *name;
34         char *value;
35         char *domain;
36         char *path;
37         struct Cookie *next;
38 } Cookie;
39
40 SoupCookieJar *cookiejar;
41 SoupSession *session;
42 Client *clients = NULL;
43 Cookie *cookies = NULL;
44 gboolean embed = FALSE;
45 gboolean showxid = FALSE;
46 gboolean ignore_once = FALSE;
47 extern char *optarg;
48 extern int optind;
49
50 static void cleanup(void);
51 static void proccookies(SoupMessage *m, Client *c);
52 static void destroyclient(Client *c);
53 static void destroywin(GtkWidget* w, Client *c);
54 static void die(char *str);
55 static void download(WebKitDownload *o, GParamSpec *pspec, Client *c);
56 static gboolean initdownload(WebKitWebView *view, WebKitDownload *o, Client *c);
57 static gchar *geturi(Client *c);
58 static void hidesearch(Client *c);
59 static void hideurl(Client *c);
60 static gboolean keypress(GtkWidget* w, GdkEventKey *ev, Client *c);
61 static void linkhover(WebKitWebView* page, const gchar* t, const gchar* l, Client *c);
62 static void loadcommit(WebKitWebView *view, WebKitWebFrame *f, Client *c);
63 static void loadstart(WebKitWebView *view, WebKitWebFrame *f, Client *c);
64 static void loadfile(Client *c, const gchar *f);
65 static void loaduri(Client *c, const gchar *uri);
66 static Client *newclient();
67 static WebKitWebView *newwindow(WebKitWebView  *v, WebKitWebFrame *f, Client *c);
68 static void pasteurl(GtkClipboard *clipboard, const gchar *text, gpointer d);
69 static GdkFilterReturn processx(GdkXEvent *xevent, GdkEvent *event, gpointer d);
70 static void progresschange(WebKitWebView *view, gint p, Client *c);
71 static void request(SoupSession *s, SoupMessage *m, Client *c);
72 static void setcookie(char *name, char *val, char *dom, char *path, long exp);
73 static void setup(void);
74 static void showsearch(Client *c);
75 static void showurl(Client *c);
76 static void stop(Client *c);
77 static void titlechange(WebKitWebView* view, WebKitWebFrame* frame,
78                 const gchar* title, Client *c);
79 static void usage();
80 static void updatetitle(Client *c, const gchar *title);
81
82 void
83 cleanup(void) {
84         while(clients)
85                 destroyclient(clients);
86 }
87
88 void
89 proccookies(SoupMessage *m, Client *c) {
90         GSList *l;
91         SoupCookie *co;
92         long t;
93
94         for (l = soup_cookies_from_response(m); l; l = l->next){
95                 co = (SoupCookie *)l->data;
96                 t = co->expires ?  soup_date_to_time_t(co->expires) : 0;
97                 setcookie(co->name, co->value, co->domain, co->value, t);
98         }
99         g_slist_free(l);
100 }
101
102 void
103 destroyclient(Client *c) {
104         Client *p;
105
106         gtk_widget_destroy(GTK_WIDGET(webkit_web_view_new()));
107         gtk_widget_destroy(c->scroll);
108         gtk_widget_destroy(c->urlbar);
109         gtk_widget_destroy(c->searchbar);
110         gtk_widget_destroy(c->vbox);
111         gtk_widget_destroy(c->win);
112         for(p = clients; p && p->next != c; p = p->next);
113         if(p)
114                 p->next = c->next;
115         else
116                 clients = c->next;
117         free(c);
118         if(clients == NULL)
119                 gtk_main_quit();
120 }
121
122 void
123 destroywin(GtkWidget* w, Client *c) {
124         destroyclient(c);
125 }
126
127 void
128 die(char *str) {
129         fputs(str, stderr);
130         exit(EXIT_FAILURE);
131 }
132
133 void
134 download(WebKitDownload *o, GParamSpec *pspec, Client *c) {
135         WebKitDownloadStatus status;
136
137         status = webkit_download_get_status(c->download);
138         if(status == WEBKIT_DOWNLOAD_STATUS_STARTED || status == WEBKIT_DOWNLOAD_STATUS_CREATED) {
139                 c->progress = (int)(webkit_download_get_progress(c->download)*100);
140         }
141         updatetitle(c, NULL);
142 }
143
144 gboolean
145 initdownload(WebKitWebView *view, WebKitDownload *o, Client *c) {
146         const gchar *home, *filename;
147         gchar *uri, *path, *html;
148
149         stop(c);
150         c->download = o;
151         home = g_get_home_dir();
152         filename = webkit_download_get_suggested_filename(o);
153         path = g_build_filename(home, ".surf", "dl", 
154                         filename, NULL);
155         uri = g_strconcat("file://", path, NULL);
156         webkit_download_set_destination_uri(c->download, uri);
157         c->progress = 0;
158         g_free(uri);
159         html = g_strdup_printf("Download <b>%s</b>...", filename);
160         webkit_web_view_load_html_string(c->view, html,
161                         webkit_download_get_uri(c->download));
162         g_signal_connect(c->download, "notify::progress", G_CALLBACK(download), c);
163         g_signal_connect(c->download, "notify::status", G_CALLBACK(download), c);
164         webkit_download_start(c->download);
165         updatetitle(c, filename);
166         g_free(html);
167         return TRUE;
168 }
169
170 gchar *
171 geturi(Client *c) {
172         gchar *uri;
173
174         if(!(uri = (gchar *)webkit_web_view_get_uri(c->view)))
175                 uri = g_strdup("about:blank");
176         return uri;
177 }
178
179 void
180 hidesearch(Client *c) {
181         gtk_widget_hide(c->searchbar);
182         gtk_widget_grab_focus(GTK_WIDGET(c->view));
183 }
184
185 void
186 hideurl(Client *c) {
187         gtk_widget_hide(c->urlbar);
188         gtk_widget_grab_focus(GTK_WIDGET(c->view));
189 }
190
191 gboolean
192 keypress(GtkWidget* w, GdkEventKey *ev, Client *c) {
193         if(ev->type != GDK_KEY_PRESS)
194                 return FALSE;
195         if(GTK_WIDGET_HAS_FOCUS(c->searchbar)) {
196                 switch(ev->keyval) {
197                 case GDK_Escape:
198                         hidesearch(c);
199                         return TRUE;
200                 case GDK_Return:
201                         webkit_web_view_search_text(c->view,
202                                         gtk_entry_get_text(GTK_ENTRY(c->searchbar)),
203                                         FALSE,
204                                         !(ev->state & GDK_SHIFT_MASK),
205                                         TRUE);
206                         return TRUE;
207                 case GDK_Left:
208                 case GDK_Right:
209                         return FALSE;
210                 }
211         }
212         else if(GTK_WIDGET_HAS_FOCUS(c->urlbar)) {
213                 switch(ev->keyval) {
214                 case GDK_Escape:
215                         hideurl(c);
216                         return TRUE;
217                 case GDK_Return:
218                         loaduri(c, gtk_entry_get_text(GTK_ENTRY(c->urlbar)));
219                         hideurl(c);
220                         return TRUE;
221                 case GDK_Left:
222                 case GDK_Right:
223                         return FALSE;
224                 }
225         }
226         if(ev->state & GDK_CONTROL_MASK) {
227                 switch(ev->keyval) {
228                 case GDK_P:
229                         webkit_web_frame_print(webkit_web_view_get_main_frame(c->view));
230                         return TRUE;
231                 case GDK_p:
232                         gtk_clipboard_request_text(gtk_clipboard_get(GDK_SELECTION_PRIMARY), pasteurl, c);
233                 case GDK_y:
234                         gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_PRIMARY), webkit_web_view_get_uri(c->view), -1);
235                         return TRUE;
236                 case GDK_r:
237                         webkit_web_view_reload(c->view);
238                         return TRUE;
239                 case GDK_R:
240                         webkit_web_view_reload_bypass_cache(c->view);
241                         return TRUE;
242                 case GDK_b:
243                         return TRUE;
244                 case GDK_g:
245                         showurl(c);
246                         return TRUE;
247                 case GDK_slash:
248                         showsearch(c);
249                         return TRUE;
250                 case GDK_plus:
251                 case GDK_equal:
252                         webkit_web_view_zoom_in(c->view);
253                         return TRUE;
254                 case GDK_minus:
255                         webkit_web_view_zoom_out(c->view);
256                         return TRUE;
257                 case GDK_0:
258                         webkit_web_view_set_zoom_level(c->view, 1.0);
259                         return TRUE;
260                 case GDK_n:
261                 case GDK_N:
262                         webkit_web_view_search_text(c->view,
263                                         gtk_entry_get_text(GTK_ENTRY(c->searchbar)),
264                                         FALSE,
265                                         !(ev->state & GDK_SHIFT_MASK),
266                                         TRUE);
267                         return TRUE;
268                 case GDK_h:
269                         webkit_web_view_go_back(c->view);
270                         return TRUE;
271                 case GDK_l:
272                         webkit_web_view_go_forward(c->view);
273                         return TRUE;
274                 }
275         }
276         else {
277                 switch(ev->keyval) {
278                 case GDK_Escape:
279                         stop(c);
280                         return TRUE;
281                 }
282         }
283         return FALSE;
284 }
285
286 void
287 linkhover(WebKitWebView* page, const gchar* t, const gchar* l, Client *c) {
288         if(l)
289                 gtk_window_set_title(GTK_WINDOW(c->win), l);
290         else
291                 updatetitle(c, NULL);
292 }
293
294 void
295 loadcommit(WebKitWebView *view, WebKitWebFrame *f, Client *c) {
296         gchar *uri;
297
298         ignore_once = TRUE;
299         uri = geturi(c);
300         XChangeProperty(dpy, GDK_WINDOW_XID(GTK_WIDGET(c->win)->window), urlprop,
301                         XA_STRING, 8, PropModeReplace, (unsigned char *)uri,
302                         strlen(uri) + 1);
303 }
304
305 void
306 loadstart(WebKitWebView *view, WebKitWebFrame *f, Client *c) {
307         c->progress = 0;
308         updatetitle(c, NULL);
309 }
310
311 void
312 loadfile(Client *c, const gchar *f) {
313         GIOChannel *chan = NULL;
314         GError *e = NULL;
315         GString *code;
316         gchar *line, *uri;
317
318         if(strcmp(f, "-") == 0) {
319                 chan = g_io_channel_unix_new(STDIN_FILENO);
320                 if (chan) {
321                         code = g_string_new("");
322                         while(g_io_channel_read_line(chan, &line, NULL, NULL,
323                                                 &e) == G_IO_STATUS_NORMAL) {
324                                 g_string_append(code, line);
325                                 g_free(line);
326                         }
327                         webkit_web_view_load_html_string(c->view, code->str,
328                                         "file://.");
329                         g_io_channel_shutdown(chan, FALSE, NULL);
330                         g_string_free(code, TRUE);
331                 }
332                 uri = g_strdup("stdin");
333         }
334         else {
335                 uri = g_strdup_printf("file://%s", f);
336                 loaduri(c, uri);
337         }
338         updatetitle(c, uri);
339         g_free(uri);
340 }
341
342 void
343 loaduri(Client *c, const gchar *uri) {
344         gchar *u;
345         u = g_strrstr(uri, "://") ? g_strdup(uri)
346                 : g_strdup_printf("http://%s", uri);
347         webkit_web_view_load_uri(c->view, u);
348         c->progress = 0;
349         updatetitle(c, u);
350         g_free(u);
351 }
352
353 Client *
354 newclient(void) {
355         Client *c;
356         if(!(c = calloc(1, sizeof(Client))))
357                 die("Cannot malloc!\n");
358         /* Window */
359         if(embed) {
360                 c->win = gtk_plug_new(0);
361         }
362         else {
363                 c->win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
364                 gtk_window_set_wmclass(GTK_WINDOW(c->win), "surf", "surf");
365         }
366         gtk_window_set_default_size(GTK_WINDOW(c->win), 800, 600);
367         g_signal_connect(G_OBJECT(c->win), "destroy", G_CALLBACK(destroywin), c);
368         g_signal_connect(G_OBJECT(c->win), "key-press-event", G_CALLBACK(keypress), c);
369
370         /* VBox */
371         c->vbox = gtk_vbox_new(FALSE, 0);
372
373         /* scrolled window */
374         c->scroll = gtk_scrolled_window_new(NULL, NULL);
375         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(c->scroll),
376                         GTK_POLICY_NEVER, GTK_POLICY_NEVER);
377
378         /* webview */
379         c->view = WEBKIT_WEB_VIEW(webkit_web_view_new());
380         g_signal_connect(G_OBJECT(c->view), "title-changed", G_CALLBACK(titlechange), c);
381         g_signal_connect(G_OBJECT(c->view), "load-progress-changed", G_CALLBACK(progresschange), c);
382         g_signal_connect(G_OBJECT(c->view), "load-committed", G_CALLBACK(loadcommit), c);
383         g_signal_connect(G_OBJECT(c->view), "load-started", G_CALLBACK(loadstart), c);
384         g_signal_connect(G_OBJECT(c->view), "hovering-over-link", G_CALLBACK(linkhover), c);
385         g_signal_connect(G_OBJECT(c->view), "create-web-view", G_CALLBACK(newwindow), c);
386         g_signal_connect(G_OBJECT(c->view), "download-requested", G_CALLBACK(initdownload), c);
387         g_signal_connect_after(session, "request-started", G_CALLBACK(request), c);
388
389         /* urlbar */
390         c->urlbar = gtk_entry_new();
391         gtk_entry_set_has_frame(GTK_ENTRY(c->urlbar), FALSE);
392
393         /* searchbar */
394         c->searchbar = gtk_entry_new();
395         gtk_entry_set_has_frame(GTK_ENTRY(c->searchbar), FALSE);
396
397         /* downloadbar */
398
399         /* Arranging */
400         gtk_container_add(GTK_CONTAINER(c->scroll), GTK_WIDGET(c->view));
401         gtk_container_add(GTK_CONTAINER(c->win), c->vbox);
402         gtk_container_add(GTK_CONTAINER(c->vbox), c->scroll);
403         gtk_container_add(GTK_CONTAINER(c->vbox), c->searchbar);
404         gtk_container_add(GTK_CONTAINER(c->vbox), c->urlbar);
405
406         /* Setup */
407         gtk_box_set_child_packing(GTK_BOX(c->vbox), c->urlbar, FALSE, FALSE, 0, GTK_PACK_START);
408         gtk_box_set_child_packing(GTK_BOX(c->vbox), c->searchbar, FALSE, FALSE, 0, GTK_PACK_START);
409         gtk_box_set_child_packing(GTK_BOX(c->vbox), c->scroll, TRUE, TRUE, 0, GTK_PACK_START);
410         gtk_widget_grab_focus(GTK_WIDGET(c->view));
411         gtk_widget_hide_all(c->searchbar);
412         gtk_widget_hide_all(c->urlbar);
413         gtk_widget_show(c->vbox);
414         gtk_widget_show(c->scroll);
415         gtk_widget_show(GTK_WIDGET(c->view));
416         gtk_widget_show(c->win);
417         gdk_window_set_events(GTK_WIDGET(c->win)->window, GDK_ALL_EVENTS_MASK);
418         gdk_window_add_filter(GTK_WIDGET(c->win)->window, processx, c);
419         webkit_web_view_set_full_content_zoom(c->view, TRUE);
420         c->download = NULL;
421         c->title = NULL;
422         c->next = clients;
423         clients = c;
424         if(showxid)
425                 printf("%u\n", (unsigned int)GDK_WINDOW_XID(GTK_WIDGET(c->win)->window));
426         return c;
427 }
428
429 WebKitWebView *
430 newwindow(WebKitWebView  *v, WebKitWebFrame *f, Client *c) {
431         Client *n = newclient();
432         return n->view;
433 }
434
435  
436 void
437 pasteurl(GtkClipboard *clipboard, const gchar *text, gpointer d) {
438         if(text != NULL)
439                 loaduri((Client *) d, text);
440 }
441
442 GdkFilterReturn
443 processx(GdkXEvent *e, GdkEvent *event, gpointer d) {
444         Client *c = (Client *)d;
445         XPropertyEvent *ev;
446         Atom adummy;
447         int idummy;
448         unsigned long ldummy;
449         unsigned char *buf = NULL;
450
451         if(((XEvent *)e)->type == PropertyNotify) {
452                 ev = &((XEvent *)e)->xproperty;
453                 if(ev->atom == urlprop && ev->state == PropertyNewValue) {
454                         if(ignore_once)
455                                ignore_once = FALSE;
456                         else {
457                                 XGetWindowProperty(dpy, ev->window, urlprop, 0L, BUFSIZ, False, XA_STRING,
458                                         &adummy, &idummy, &ldummy, &ldummy, &buf);
459                                 loaduri(c, (gchar *)buf);
460                                 XFree(buf);
461                         }
462                         return GDK_FILTER_REMOVE;
463                 }
464         }
465         return GDK_FILTER_CONTINUE;
466 }
467
468 void
469 progresschange(WebKitWebView* view, gint p, Client *c) {
470         c->progress = p;
471         updatetitle(c, NULL);
472 }
473
474 void
475 request(SoupSession *s, SoupMessage *m, Client *c) {
476         soup_message_add_header_handler(m, "got-headers", "Set-Cookie",
477                         G_CALLBACK(proccookies), c);
478 }
479
480 void
481 setcookie(char *name, char *val, char *dom, char *path, long exp) {
482         printf("%s %s %s %s %li\n", name, val, dom, path, exp);
483 }
484
485 void
486 setup(void) {
487         dpy = GDK_DISPLAY();
488         session = webkit_get_default_session();
489         urlprop = XInternAtom(dpy, "_SURF_URL", False);
490 }
491
492 void
493 showsearch(Client *c) {
494         hideurl(c);
495         gtk_widget_show(c->searchbar);
496         gtk_widget_grab_focus(c->searchbar);
497 }
498
499 void
500 showurl(Client *c) {
501         gchar *uri;
502
503         hidesearch(c);
504         uri = geturi(c);
505         gtk_entry_set_text(GTK_ENTRY(c->urlbar), uri);
506         gtk_widget_show(c->urlbar);
507         gtk_widget_grab_focus(c->urlbar);
508 }
509
510 void
511 stop(Client *c) {
512         if(c->download)
513                 webkit_download_cancel(c->download);
514         else
515                 webkit_web_view_stop_loading(c->view);
516         c->download = NULL;
517 }
518
519 void
520 titlechange(WebKitWebView *v, WebKitWebFrame *f, const gchar *t, Client *c) {
521         updatetitle(c, t);
522 }
523
524 void
525 usage() {
526         fputs("surf - simple browser\n", stderr);
527         die("usage: surf [-e] [-x] [-u uri] [-f file]\n");
528 }
529
530 void
531 updatetitle(Client *c, const char *title) {
532         gchar *t;
533
534         if(title) {
535                 if(c->title)
536                         g_free(c->title);
537                 c->title = g_strdup(title);
538         }
539         if(c->progress == 100)
540                 t = g_strdup(c->title);
541         else
542                 t = g_strdup_printf("%s [%i%%]", c->title, c->progress);
543         gtk_window_set_title(GTK_WINDOW(c->win), t);
544         g_free(t);
545
546 }
547
548 int main(int argc, char *argv[]) {
549         SoupSession *s;
550         Client *c;
551         int o;
552         const gchar *home, *filename;
553
554         gtk_init(NULL, NULL);
555         if (!g_thread_supported())
556                 g_thread_init(NULL);
557         setup();
558         while((o = getopt(argc, argv, "vhxeu:f:")) != -1)
559                 switch(o) {
560                 case 'x':
561                         showxid = TRUE;
562                         break;
563                 case 'e':
564                         showxid = TRUE;
565                         embed = TRUE;
566                         break;
567                 case 'u':
568                         c = newclient();
569                         loaduri(c, optarg);
570                         break;
571                 case 'f':
572                         c = newclient();
573                         loadfile(c, optarg);
574                         break;
575                 case 'v':
576                         die("surf-"VERSION", © 2009 surf engineers, see LICENSE for details\n");
577                         break;
578                 default:
579                         usage();
580                 }
581         if(optind != argc)
582                 usage();
583         if(!clients)
584                 newclient();
585
586         /* make dirs */
587         home = g_get_home_dir();
588         filename = g_build_filename(home, ".surf", NULL);
589         g_mkdir_with_parents(filename, 0711);
590         filename = g_build_filename(home, ".surf", "dl", NULL);
591         g_mkdir_with_parents(filename, 0755);
592
593         /* cookie persistance */
594         s = webkit_get_default_session();
595         filename = g_build_filename(home, ".surf", "cookies", NULL);
596         cookiejar = soup_cookie_jar_text_new(filename, FALSE);
597         soup_session_add_feature(s, SOUP_SESSION_FEATURE(cookiejar));
598
599         gtk_main();
600         cleanup();
601         return EXIT_SUCCESS;
602 }