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