#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; }