From 189ec51e21ff0690b26083236e7cbf83bc0fa76d Mon Sep 17 00:00:00 2001 From: Avitld Date: Mon, 28 Jul 2025 14:34:35 +0300 Subject: Patch 1 --- Makefile | 28 ++++ README.md | 16 +++ config.h | 22 +++ screenshot.png | Bin 0 -> 21582 bytes webb.c | 415 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 481 insertions(+) create mode 100644 Makefile create mode 100644 README.md create mode 100644 config.h create mode 100644 screenshot.png create mode 100644 webb.c 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 Binary files /dev/null and b/screenshot.png 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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; +} -- cgit v1.2.3