aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile28
-rw-r--r--README.md16
-rw-r--r--config.h22
-rw-r--r--screenshot.pngbin0 -> 21582 bytes
-rw-r--r--webb.c415
5 files changed, 481 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..e3f136d
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,28 @@
+NAME = webb
+SRC = webb.c
+CONFIG = config.h
+PREFIX ?= /usr/local
+BINDIR ?= $(PREFIX)/bin
+
+CC = gcc
+PKGCONF = pkgconf
+CFLAGS = `$(PKGCONF) --cflags gtk+-3.0 webkit2gtk-4.0`
+LIBS = `$(PKGCONF) --libs gtk+-3.0 webkit2gtk-4.0` -lX11
+
+all: $(NAME)
+
+$(NAME): $(SRC) $(CONFIG)
+ $(CC) -o $(NAME) $(SRC) $(CFLAGS) $(LIBS)
+
+install: $(NAME)
+ mkdir -p $(BINDIR)
+ cp -f $(NAME) $(BINDIR)/
+ chmod 755 $(BINDIR)/$(NAME)
+
+uninstall:
+ rm -f $(BINDIR)/$(NAME)
+
+clean:
+ rm -f $(NAME)
+
+.PHONY: all install uninstall clean
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ac7b4ba
--- /dev/null
+++ b/README.md
@@ -0,0 +1,16 @@
+# webb
+## webb is an extremely minimal web browser written in Webkit2GTK and XLib
+
+webb (literally **web b**rowser) is a very simplistic web browser written in C.
+
+features:
+- tabs
+- downloads (requires wget, only downloads to ~/Downloads)
+
+basic commands include:
+- ctrl + o = open url
+- ctrl + g = go back
+- ctrl + f = go forward
+- ctrl + r = refresh
+- ctrl + q = close tab
+- ctrl + t = new tab
diff --git a/config.h b/config.h
new file mode 100644
index 0000000..e351c45
--- /dev/null
+++ b/config.h
@@ -0,0 +1,22 @@
+#define TAB_ACTIVE_COLOR "#7d4f9e"
+#define TAB_ACTIVE_TEXT_COLOR "#ffffff"
+
+void new_tab(void);
+void close_tab(void);
+void reload_tab(void);
+void toggle_url_bar(void);
+void go_back(void);
+void go_forward(void);
+
+static struct {
+ guint keyval;
+ GdkModifierType mod;
+ void (*func)(void);
+} keys[] = {
+ { GDK_KEY_t, GDK_CONTROL_MASK, new_tab },
+ { GDK_KEY_q, GDK_CONTROL_MASK, close_tab },
+ { GDK_KEY_r, GDK_CONTROL_MASK, reload_tab },
+ { GDK_KEY_o, GDK_CONTROL_MASK, toggle_url_bar },
+ { GDK_KEY_g, GDK_CONTROL_MASK, go_back },
+ { GDK_KEY_f, GDK_CONTROL_MASK, go_forward }
+};
diff --git a/screenshot.png b/screenshot.png
new file mode 100644
index 0000000..427e187
--- /dev/null
+++ b/screenshot.png
Binary files differ
diff --git a/webb.c b/webb.c
new file mode 100644
index 0000000..68221f1
--- /dev/null
+++ b/webb.c
@@ -0,0 +1,415 @@
+#include <gtk/gtk.h>
+#include <webkit2/webkit2.h>
+#include <gdk/gdkx.h>
+#include <X11/Xlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include "config.h"
+
+#define URL_INPUT_MAX 2048
+#define STATUSBAR_HEIGHT 20
+#define TABBAR_HEIGHT 20
+#define MAX_TABS 64
+#define DATA_DIR ".webb"
+
+typedef struct {
+ WebKitWebView *view;
+ char title[128];
+ char url[URL_INPUT_MAX];
+} Tab;
+
+static char *startup_url = NULL;
+static GtkWidget *window = NULL;
+static GtkWidget *main_box = NULL;
+static GtkWidget *tabbar = NULL;
+static GtkWidget *statusbar = NULL;
+static GtkWidget *web_stack = NULL;
+
+static Display *xdisplay = NULL;
+static GC xgc;
+static int url_input_mode = 0;
+static char url_input_buffer[URL_INPUT_MAX] = {0};
+static int url_input_len = 0;
+static char current_url[URL_INPUT_MAX] = {0};
+
+static Tab tabs[MAX_TABS];
+static int tab_count = 0;
+static int current_tab = 0;
+
+static unsigned long tab_active_pixel = 0;
+static unsigned long tab_active_text_pixel = 0;
+
+WebKitWebContext *web_context = NULL;
+
+static int parse_hex_color(const char *hex, XColor *c) {
+ if (!hex || strlen(hex) != 7 || hex[0] != '#') return 0;
+ char rs[3] = {hex[1], hex[2], 0};
+ char gs[3] = {hex[3], hex[4], 0};
+ char bs[3] = {hex[5], hex[6], 0};
+ c->red = (unsigned short)(strtoul(rs, NULL, 16) * 257);
+ c->green = (unsigned short)(strtoul(gs, NULL, 16) * 257);
+ c->blue = (unsigned short)(strtoul(bs, NULL, 16) * 257);
+ c->flags = DoRed | DoGreen | DoBlue;
+ return 1;
+}
+
+static void ensure_tab_active_color(void) {
+ if (!xdisplay) return;
+ if (tab_active_pixel && tab_active_text_pixel) return;
+ Colormap cmap = DefaultColormap(xdisplay, DefaultScreen(xdisplay));
+ XColor bg = {0}, fg = {0};
+ if (!parse_hex_color(TAB_ACTIVE_COLOR, &bg)) bg.pixel = WhitePixel(xdisplay, DefaultScreen(xdisplay));
+ if (!parse_hex_color(TAB_ACTIVE_TEXT_COLOR, &fg)) fg.pixel = BlackPixel(xdisplay, DefaultScreen(xdisplay));
+ if (XAllocColor(xdisplay, cmap, &bg))
+ tab_active_pixel = bg.pixel;
+ else
+ tab_active_pixel = WhitePixel(xdisplay, DefaultScreen(xdisplay));
+ if (XAllocColor(xdisplay, cmap, &fg))
+ tab_active_text_pixel = fg.pixel;
+ else
+ tab_active_text_pixel = BlackPixel(xdisplay, DefaultScreen(xdisplay));
+}
+
+static gboolean on_tabbar_draw(GtkWidget *widget, cairo_t *cr, gpointer user_data) {
+ GdkWindow *gdk_window = gtk_widget_get_window(widget);
+ Window xid = GDK_WINDOW_XID(gdk_window);
+ if (!xdisplay) xdisplay = XOpenDisplay(NULL);
+ if (!xgc && xdisplay && xid) xgc = XCreateGC(xdisplay, xid, 0, NULL);
+ ensure_tab_active_color();
+
+ GtkAllocation allocation;
+ gtk_widget_get_allocation(widget, &allocation);
+ int win_width = allocation.width;
+
+ XSetForeground(xdisplay, xgc, BlackPixel(xdisplay, DefaultScreen(xdisplay)));
+ XFillRectangle(xdisplay, xid, xgc, 0, 0, win_width, TABBAR_HEIGHT);
+
+ for (int i = 0; i < tab_count; ++i) {
+ int tab_w = win_width / (tab_count ? tab_count : 1);
+ int tab_x = i * tab_w;
+ if (i == current_tab) {
+ XSetForeground(xdisplay, xgc, tab_active_pixel);
+ XFillRectangle(xdisplay, xid, xgc, tab_x, 0, tab_w, TABBAR_HEIGHT);
+ XSetForeground(xdisplay, xgc, tab_active_text_pixel);
+ } else {
+ XSetForeground(xdisplay, xgc, BlackPixel(xdisplay, DefaultScreen(xdisplay)));
+ XFillRectangle(xdisplay, xid, xgc, tab_x, 0, tab_w, TABBAR_HEIGHT);
+ XSetForeground(xdisplay, xgc, WhitePixel(xdisplay, DefaultScreen(xdisplay)));
+ }
+ const char *label = tabs[i].title[0] ? tabs[i].title : tabs[i].url;
+ XDrawString(xdisplay, xid, xgc, tab_x + 8, TABBAR_HEIGHT - 5, label, strlen(label));
+ }
+ XFlush(xdisplay);
+ return FALSE;
+}
+
+static gboolean on_statusbar_draw(GtkWidget *widget, cairo_t *cr, gpointer user_data) {
+ GdkWindow *gdk_window = gtk_widget_get_window(widget);
+ Window xid = GDK_WINDOW_XID(gdk_window);
+ if (!xdisplay) xdisplay = XOpenDisplay(NULL);
+ if (!xgc && xdisplay && xid) xgc = XCreateGC(xdisplay, xid, 0, NULL);
+
+ GtkAllocation allocation;
+ gtk_widget_get_allocation(widget, &allocation);
+ int win_width = allocation.width;
+
+ XSetForeground(xdisplay, xgc, BlackPixel(xdisplay, DefaultScreen(xdisplay)));
+ XFillRectangle(xdisplay, xid, xgc, 0, 0, win_width, STATUSBAR_HEIGHT);
+ XSetForeground(xdisplay, xgc, WhitePixel(xdisplay, DefaultScreen(xdisplay)));
+
+ char bar_text[URL_INPUT_MAX + 64];
+ if (url_input_mode)
+ snprintf(bar_text, sizeof(bar_text), "Enter URL: %s", url_input_buffer);
+ else
+ snprintf(bar_text, sizeof(bar_text), "%s", current_url);
+ XDrawString(xdisplay, xid, xgc, 10, 15, bar_text, strlen(bar_text));
+ XFlush(xdisplay);
+ return FALSE;
+}
+
+static void redraw_tabbar(void) {
+ if (tabbar) gtk_widget_queue_draw(tabbar);
+}
+static void redraw_statusbar(void) {
+ if (statusbar) gtk_widget_queue_draw(statusbar);
+}
+
+static void on_tabbar_realize(GtkWidget *widget, gpointer user_data) {
+ ensure_tab_active_color();
+ redraw_tabbar();
+}
+static void on_statusbar_realize(GtkWidget *widget, gpointer user_data) {
+ redraw_statusbar();
+}
+
+static void set_current_url(const char *uri) {
+ strncpy(current_url, uri ? uri : "", URL_INPUT_MAX - 1);
+ current_url[URL_INPUT_MAX - 1] = 0;
+ if (tab_count > 0) {
+ strncpy(tabs[current_tab].url, uri ? uri : "", URL_INPUT_MAX - 1);
+ tabs[current_tab].url[URL_INPUT_MAX - 1] = 0;
+ }
+ redraw_statusbar();
+ redraw_tabbar();
+}
+
+static void on_title_changed(WebKitWebView *view, GParamSpec *pspec, gpointer user_data) {
+ if (tab_count > 0) {
+ const gchar *title = webkit_web_view_get_title(view);
+ strncpy(tabs[current_tab].title, title ? title : "", sizeof(tabs[current_tab].title) - 1);
+ tabs[current_tab].title[sizeof(tabs[current_tab].title) - 1] = 0;
+ redraw_tabbar();
+ }
+}
+static void on_uri_changed(WebKitWebView *view, GParamSpec *pspec, gpointer user_data) {
+ const gchar *uri = webkit_web_view_get_uri(view);
+ set_current_url(uri);
+}
+
+static void on_download_started(WebKitWebContext *context, WebKitDownload *download, gpointer user_data) {
+ WebKitURIRequest *req = webkit_download_get_request(download);
+ const gchar *uri = webkit_uri_request_get_uri(req);
+ const char *downloaddir = g_get_user_special_dir(G_USER_DIRECTORY_DOWNLOAD);
+ if (!downloaddir) downloaddir = g_get_home_dir();
+ if (fork() == 0) {
+ setsid();
+ chdir(downloaddir);
+ execlp("wget", "wget", uri, (char *)NULL);
+ _exit(1);
+ }
+ webkit_download_cancel(download);
+}
+
+static WebKitWebView* on_create_web_view(WebKitWebView *web_view, WebKitNavigationAction *nav, gpointer user_data) {
+ WebKitWebView *new_view = WEBKIT_WEB_VIEW(webkit_web_view_new_with_context(web_context));
+ g_signal_connect(new_view, "notify::uri", G_CALLBACK(on_uri_changed), NULL);
+ g_signal_connect(new_view, "notify::title", G_CALLBACK(on_title_changed), NULL);
+ g_signal_connect(new_view, "create", G_CALLBACK(on_create_web_view), NULL);
+ return new_view;
+}
+
+void new_tab(void) {
+ if (tab_count < MAX_TABS) {
+ WebKitWebView *new_view = WEBKIT_WEB_VIEW(webkit_web_view_new_with_context(web_context));
+ tabs[tab_count].view = new_view;
+ tabs[tab_count].title[0] = 0;
+ strcpy(tabs[tab_count].url, "about:blank");
+ gtk_container_add(GTK_CONTAINER(web_stack), GTK_WIDGET(new_view));
+ g_signal_connect(new_view, "notify::uri", G_CALLBACK(on_uri_changed), NULL);
+ g_signal_connect(new_view, "notify::title", G_CALLBACK(on_title_changed), NULL);
+ g_signal_connect(new_view, "create", G_CALLBACK(on_create_web_view), NULL);
+ gtk_widget_show(GTK_WIDGET(new_view));
+ current_tab = tab_count;
+ tab_count++;
+ gtk_stack_set_visible_child(GTK_STACK(web_stack), GTK_WIDGET(new_view));
+ set_current_url("about:blank");
+ redraw_tabbar();
+ }
+}
+void close_tab(void) {
+ if (tab_count == 1) {
+ gtk_window_close(GTK_WINDOW(window));
+ return;
+ }
+ GtkWidget *webview = GTK_WIDGET(tabs[current_tab].view);
+ gtk_container_remove(GTK_CONTAINER(web_stack), webview);
+ for (int i = current_tab; i < tab_count - 1; ++i)
+ tabs[i] = tabs[i + 1];
+ tab_count--;
+ if (current_tab >= tab_count)
+ current_tab = tab_count - 1;
+ gtk_stack_set_visible_child(GTK_STACK(web_stack), GTK_WIDGET(tabs[current_tab].view));
+ set_current_url(tabs[current_tab].url);
+ redraw_tabbar();
+}
+void reload_tab(void) {
+ if (tab_count > 0)
+ webkit_web_view_reload(tabs[current_tab].view);
+}
+void toggle_url_bar(void) {
+ url_input_mode = !url_input_mode;
+ redraw_statusbar();
+}
+
+void go_forward(void) {
+ if (tab_count > 0)
+ webkit_web_view_go_forward(tabs[current_tab].view);
+}
+void go_back(void) {
+ if (tab_count > 0)
+ webkit_web_view_go_back(tabs[current_tab].view);
+}
+
+static gboolean on_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data) {
+ for (unsigned int i = 0; i < sizeof(keys)/sizeof(keys[0]); ++i) {
+ if ((event->state & keys[i].mod) == keys[i].mod && event->keyval == keys[i].keyval) {
+ keys[i].func();
+ return TRUE;
+ }
+ }
+ if (url_input_mode) {
+ if (event->keyval == GDK_KEY_Return || event->keyval == GDK_KEY_KP_Enter) {
+ if (url_input_len > 0) {
+ char *uri;
+ if (g_str_has_prefix(url_input_buffer, "http://") || g_str_has_prefix(url_input_buffer, "https://")) {
+ uri = g_strdup(url_input_buffer);
+ } else {
+ uri = g_strdup_printf("https://%s", url_input_buffer);
+ }
+ webkit_web_view_load_uri(tabs[current_tab].view, uri);
+ g_free(uri);
+ }
+ url_input_buffer[0] = 0;
+ url_input_len = 0;
+ url_input_mode = 0;
+ redraw_statusbar();
+ return TRUE;
+ } else if (event->keyval == GDK_KEY_Escape) {
+ url_input_buffer[0] = 0;
+ url_input_len = 0;
+ url_input_mode = 0;
+ redraw_statusbar();
+ return TRUE;
+ } else if (event->keyval == GDK_KEY_BackSpace) {
+ if (url_input_len > 0) {
+ url_input_len--;
+ url_input_buffer[url_input_len] = '\0';
+ redraw_statusbar();
+ }
+ } else if (event->string && g_ascii_isprint(event->string[0])) {
+ if (url_input_len < URL_INPUT_MAX - 1) {
+ url_input_buffer[url_input_len++] = event->string[0];
+ url_input_buffer[url_input_len] = '\0';
+ redraw_statusbar();
+ }
+ }
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static gboolean on_tabbar_click(GtkWidget *widget, GdkEventButton *event, gpointer user_data) {
+ if (tab_count < 2) return FALSE;
+ GtkAllocation allocation;
+ gtk_widget_get_allocation(widget, &allocation);
+ int win_width = allocation.width;
+ int tab_w = win_width / tab_count;
+ int idx = (int)(event->x) / tab_w;
+ if (idx >= 0 && idx < tab_count && idx != current_tab) {
+ current_tab = idx;
+ gtk_stack_set_visible_child(GTK_STACK(web_stack), GTK_WIDGET(tabs[current_tab].view));
+ set_current_url(tabs[current_tab].url);
+ redraw_tabbar();
+ }
+ return TRUE;
+}
+
+static int on_command_line(GApplication *app, GApplicationCommandLine *cmdline, gpointer user_data) {
+ int argc;
+ char **argv = g_application_command_line_get_arguments(cmdline, &argc);
+ if (argc > 1) {
+ const char *arg = argv[1];
+ if (g_str_has_prefix(arg, "http://") || g_str_has_prefix(arg, "https://")) {
+ startup_url = g_strdup(arg);
+ } else {
+ startup_url = g_strdup_printf("https://%s", arg);
+ }
+ }
+ g_application_activate(app);
+ g_strfreev(argv);
+ return 0;
+}
+
+static void activate(GtkApplication *app, gpointer user_data) {
+ const char *url = startup_url ? startup_url : "about:blank";
+ char *base_dir = g_build_filename(g_get_home_dir(), DATA_DIR, NULL);
+ char *cache_dir = g_build_filename(g_get_home_dir(), DATA_DIR, "cache", NULL);
+ char *cookie_path = g_build_filename(g_get_home_dir(), DATA_DIR, "cookies.sqlite", NULL);
+
+ g_mkdir_with_parents(base_dir, 0700);
+ g_mkdir_with_parents(cache_dir, 0700);
+
+ WebKitWebsiteDataManager *data_manager = webkit_website_data_manager_new(
+ "base-data-directory", base_dir,
+ "base-cache-directory", cache_dir,
+ NULL);
+
+ web_context = webkit_web_context_new_with_website_data_manager(data_manager);
+
+ WebKitCookieManager *cookieman = webkit_web_context_get_cookie_manager(web_context);
+ webkit_cookie_manager_set_persistent_storage(
+ cookieman,
+ cookie_path,
+ WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE
+ );
+ webkit_cookie_manager_set_accept_policy(
+ cookieman,
+ WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS
+ );
+
+ g_signal_connect(web_context, "download-started", G_CALLBACK(on_download_started), NULL);
+
+ window = gtk_application_window_new(app);
+ gtk_window_set_default_size(GTK_WINDOW(window), 800, 600);
+ gtk_window_set_title(GTK_WINDOW(window), "webb");
+
+ main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_container_add(GTK_CONTAINER(window), main_box);
+
+ tabbar = gtk_drawing_area_new();
+ gtk_widget_set_size_request(tabbar, -1, TABBAR_HEIGHT);
+ gtk_box_pack_start(GTK_BOX(main_box), tabbar, FALSE, FALSE, 0);
+ g_signal_connect(tabbar, "realize", G_CALLBACK(on_tabbar_realize), NULL);
+ g_signal_connect(tabbar, "draw", G_CALLBACK(on_tabbar_draw), NULL);
+ g_signal_connect(tabbar, "button-press-event", G_CALLBACK(on_tabbar_click), NULL);
+ gtk_widget_add_events(tabbar, GDK_BUTTON_PRESS_MASK);
+
+ web_stack = gtk_stack_new();
+ gtk_box_pack_start(GTK_BOX(main_box), web_stack, TRUE, TRUE, 0);
+
+ statusbar = gtk_drawing_area_new();
+ gtk_widget_set_size_request(statusbar, -1, STATUSBAR_HEIGHT);
+ gtk_box_pack_end(GTK_BOX(main_box), statusbar, FALSE, FALSE, 0);
+ g_signal_connect(statusbar, "realize", G_CALLBACK(on_statusbar_realize), NULL);
+ g_signal_connect(statusbar, "draw", G_CALLBACK(on_statusbar_draw), NULL);
+
+ WebKitWebView *initial_view = WEBKIT_WEB_VIEW(webkit_web_view_new_with_context(web_context));
+ tabs[0].view = initial_view;
+ tabs[0].title[0] = 0;
+ strncpy(tabs[0].url, url, URL_INPUT_MAX - 1);
+ tab_count = 1;
+ current_tab = 0;
+ gtk_container_add(GTK_CONTAINER(web_stack), GTK_WIDGET(initial_view));
+ gtk_widget_show(GTK_WIDGET(initial_view));
+ g_signal_connect(initial_view, "notify::uri", G_CALLBACK(on_uri_changed), NULL);
+ g_signal_connect(initial_view, "notify::title", G_CALLBACK(on_title_changed), NULL);
+ g_signal_connect(initial_view, "create", G_CALLBACK(on_create_web_view), NULL);
+ webkit_web_view_load_uri(initial_view, url);
+
+ set_current_url(url);
+
+ if (!xdisplay) xdisplay = XOpenDisplay(NULL);
+
+ g_signal_connect(window, "key-press-event", G_CALLBACK(on_key_press), NULL);
+
+ gtk_widget_show_all(window);
+
+ g_free(base_dir);
+ g_free(cache_dir);
+ g_free(cookie_path);
+}
+
+int main(int argc, char **argv) {
+ GtkApplication *app = gtk_application_new("org.example.webb", G_APPLICATION_HANDLES_COMMAND_LINE);
+ g_signal_connect(app, "command-line", G_CALLBACK(on_command_line), NULL);
+ g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
+ int status = g_application_run(G_APPLICATION(app), argc, argv);
+ g_free(startup_url);
+ g_object_unref(app);
+ if (xdisplay) XCloseDisplay(xdisplay);
+ return status;
+}