browse

browse.git
git clone git://git.lenczewski.org/browse.git
Log | Files | Refs | README | LICENSE

commit f5f8f1a38f0e6c40188d6bada420f091e419ba34
Author: Mikołaj Lenczewski <mblenczewski@gmail.com>
Date:   Sun, 29 Oct 2023 15:38:37 +0000

Initial commit

Diffstat:
A.editorconfig | 17+++++++++++++++++
A.gitignore | 8++++++++
ALICENSE | 21+++++++++++++++++++++
AMakefile | 50++++++++++++++++++++++++++++++++++++++++++++++++++
AREADME | 3+++
Abrowse.c | 281+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abrowse.h | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aconfig.def.h | 46++++++++++++++++++++++++++++++++++++++++++++++
Aconfig.mk | 12++++++++++++
9 files changed, 532 insertions(+), 0 deletions(-)

diff --git a/.editorconfig b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +guidelines = 80, 120, 160 + +[*.{c,h}] +indent_style = tab +indent_size = 8 + +[*.{md,txt}] +indent_style = space +indent_size = 2 diff --git a/.gitignore b/.gitignore @@ -0,0 +1,8 @@ +obj/ + +browse +browse.tar + +config.h + +**/.*.swp diff --git a/LICENSE b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Mikołaj Lenczewski <mblenczewski@gmail.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile @@ -0,0 +1,50 @@ +.PHONY: all build clean dist distclean install uninstall + +include config.mk + +TARGET := browse + +SOURCES := browse.c +OBJECTS := $(SOURCES:%=$(OBJ)/%.o) +OBJDEPS := $(OBJECTS:%.o=%.d) + +ARCHIVE := $(TARGET).tar + +AUX := Makefile config.mk config.def.h browse.h browse.c + +all: build + +build: $(TARGET) + +clean: + rm -rf $(OBJ) $(TARGET) + +dist: build $(AUX) + tar cf $(ARCHIVE) $(TARGET) $(AUX) + +distclean: clean + rm -rf $(ARCHIVE) config.h + +install: build + mkdir -p $(DESTDIR)$(PREFIX)/bin + install -m 0755 $(TARGET) $(DESTDIR)$(PREFIX)/bin/$(TARGET) + +uninstall: + rm -f $(DESTDIR)$(PREFIX)/bin/$(TARGET) + +config.h: config.def.h + cp $< $@ + +$(TARGET): $(OBJECTS) | $(BIN) + $(CC) -o $@ $^ $(LDFLAGS) + +$(OBJECTS): config.h + +$(OBJECTS): $(OBJ)/%.c.o: %.c | $(OBJ) + @mkdir -p $(dir $@) + $(CC) -MMD -o $@ -c $< $(CFLAGS) $(CPPFLAGS) + +-include $(OBJDEPS) + +$(OBJ): + mkdir $@ diff --git a/README b/README @@ -0,0 +1,3 @@ +browse +------------------------------------------------------------------------------- +A browser built on webkitgtk-6.0. For build instructions look at the Makefile. diff --git a/browse.c b/browse.c @@ -0,0 +1,281 @@ +#include "browse.h" + +#include "config.h" + +static struct browse_window * +browse_new_window(struct browse_ctx *ctx, char const *uri, struct browse_window *root); + +static void +browse_del_window(struct browse_window *window); + +static void +browse_update_title(struct browse_window *ctx, char const *uri); + +static void +browse_load_uri(struct browse_window *ctx, char const *uri); + +static void +browse_on_window_destroy(GtkWindow *window, struct browse_window *ctx); + +static gboolean +browse_on_key_pressed(GtkEventController *controller, guint keyval, guint keycode, + GdkModifierType state, struct browse_window *ctx); + +static void +browse_on_load_changed(WebKitWebView *webview, WebKitLoadEvent ev, struct browse_window *ctx); + +static struct browse_ctx ctx; + +int +main(int argc, char **argv) +{ + (void) argc; + (void) argv; + + memset(&ctx, 0, sizeof ctx); + + gtk_init(); + + ctx.gtk_settings = gtk_settings_get_default(); + for (size_t i = 0; i < ARRLEN(gtk_settings); i++) { + g_object_set(G_OBJECT(ctx.gtk_settings), gtk_settings[i].name, gtk_settings[i].v, NULL); + } + + ctx.webkit_settings = webkit_settings_new(); + for (size_t i = 0; i < ARRLEN(webkit_settings); i++) { + g_object_set(G_OBJECT(ctx.webkit_settings), webkit_settings[i].name, webkit_settings[i].v, NULL); + } + + ctx.root = browse_new_window(&ctx, start_page, NULL); + + while (!ctx.shutdown) + g_main_context_iteration(NULL, TRUE); + + exit(EXIT_SUCCESS); +} + +static struct browse_window * +browse_new_window(struct browse_ctx *ctx, char const *uri, struct browse_window *root) +{ + assert(ctx); + assert(uri); + + (void) root; + + struct browse_window *window = malloc(sizeof *window); + if (!window) return NULL; + + window->window = GTK_WINDOW(gtk_window_new()); + window->event_handler = gtk_event_controller_key_new(); + + gtk_widget_add_controller(GTK_WIDGET(window->window), window->event_handler); + + g_signal_connect(window->event_handler, "key-pressed", G_CALLBACK(browse_on_key_pressed), window); + g_signal_connect(window->window, "destroy", G_CALLBACK(browse_on_window_destroy), window); + + window->clipboard = gtk_widget_get_primary_clipboard(GTK_WIDGET(window->window)); + + window->webview = WEBKIT_WEB_VIEW(webkit_web_view_new()); + + g_signal_connect(window->webview, "load-changed", G_CALLBACK(browse_on_load_changed), window); + + webkit_web_view_set_settings(window->webview, ctx->webkit_settings); + + gtk_window_set_child(window->window, GTK_WIDGET(window->webview)); + + browse_load_uri(window, uri); + browse_update_title(window, NULL); + + window->next = window->prev = NULL; + + gtk_window_present(window->window); + + return window; +} + +static void +browse_del_window(struct browse_window *window) +{ + if (window->prev) window->prev->next = window->next; + if (window->next) window->next->prev = window->prev; + + if (ctx.root == window) ctx.shutdown = true; + + free(window); +} + +static void +browse_update_title(struct browse_window *ctx, char const *uri) +{ + if (uri) snprintf(ctx->title, sizeof ctx->title, "%s", uri); + + gtk_window_set_title(ctx->window, ctx->title); +} + +static void +browse_load_uri(struct browse_window *ctx, char const *uri) +{ + assert(uri); + + if (g_str_has_prefix(uri, "http://") || g_str_has_prefix(uri, "https://") || + g_str_has_prefix(uri, "file://") || g_str_has_prefix(uri, "about:")) { + webkit_web_view_load_uri(ctx->webview, uri); + } else { + snprintf(ctx->url, sizeof ctx->url, search_page, uri); + webkit_web_view_load_uri(ctx->webview, ctx->url); + } +} + +static gboolean +browse_on_key_pressed(GtkEventController *controller, guint keyval, guint keycode, + GdkModifierType state, struct browse_window *ctx) +{ + (void) controller; + (void) keycode; + + for (size_t i = 0; i < ARRLEN(keybinds); i++) { + if ((state & GDK_MODIFIER_MASK) == keybinds[i].mod && keyval == keybinds[i].key) { + keybinds[i].handler(ctx, &keybinds[i].arg); + return TRUE; + } + } + + return FALSE; +} + +static void +browse_on_window_destroy(GtkWindow *window, struct browse_window *ctx) +{ + (void) window; + + browse_del_window(ctx); +} + +static void +browse_on_load_changed(WebKitWebView *webview, WebKitLoadEvent ev, struct browse_window *ctx) +{ + (void) ev; + + char const *uri = webkit_web_view_get_uri(webview); + browse_update_title(ctx, uri); +} + +void +stopload(struct browse_window *ctx, union browse_keybind_arg const *arg) +{ + assert(ctx); + + (void) arg; + + webkit_web_view_stop_loading(ctx->webview); +} + +void +reload(struct browse_window *ctx, union browse_keybind_arg const *arg) +{ + assert(ctx); + assert(arg); + + if (arg->i) { + webkit_web_view_reload_bypass_cache(ctx->webview); + } else { + webkit_web_view_reload(ctx->webview); + } +} + +void +navigate(struct browse_window *ctx, union browse_keybind_arg const *arg) +{ + assert(ctx); + assert(arg); + + if (arg->i < 0) { + webkit_web_view_go_back(ctx->webview); + } else if (arg->i > 0) { + webkit_web_view_go_forward(ctx->webview); + } +} + +static void +clipboard_cb(GObject *src, GAsyncResult *res, void *user_data) +{ + (void) src; + + struct browse_window *ctx = user_data; + + char *text; + if ((text = gdk_clipboard_read_text_finish(GDK_CLIPBOARD(src), res, NULL))) { + browse_load_uri(ctx, text); + } +} + +void +clipboard(struct browse_window *ctx, union browse_keybind_arg const *arg) +{ + assert(ctx); + assert(arg); + + if (arg->i) { + gdk_clipboard_read_text_async(ctx->clipboard, NULL, clipboard_cb, ctx); + } else { + gdk_clipboard_set_text(ctx->clipboard, webkit_web_view_get_uri(ctx->webview)); + } +} + +static void +javascript_cb(GObject *src, GAsyncResult *res, void *user_data) +{ + (void) user_data; + + JSCValue *jsres; + if ((jsres = webkit_web_view_evaluate_javascript_finish(WEBKIT_WEB_VIEW(src), res, NULL))) { + // TODO: how to correctly free jsres, which we apparently own now? + } +} + +void +javascript(struct browse_window *ctx, union browse_keybind_arg const *arg) +{ + assert(ctx); + assert(arg); + + webkit_web_view_evaluate_javascript(ctx->webview, arg->s, -1, + NULL, /* world */ + NULL, /* source_uri */ + NULL, javascript_cb, NULL); +} + +static char * +spawn(char const *cmd) +{ + FILE *pstdout = popen(cmd, "r"); + if (!pstdout) return NULL; + + char *line = NULL; + size_t len; + + if (getline(&line, &len, pstdout) == -1) { + pclose(pstdout); + return NULL; + } + + pclose(pstdout); + + (void) len; + + return line; +} + +void +search(struct browse_window *ctx, union browse_keybind_arg const *arg) +{ + assert(ctx); + assert(arg); + + char *uri = spawn(arg->s); + if (!uri) return; + + browse_load_uri(ctx, uri); + + free(uri); +} diff --git a/browse.h b/browse.h @@ -0,0 +1,94 @@ +#ifndef BROWSE_H +#define BROWSE_H + +#ifdef _XOPEN_SOURCE +#undef _XOPEN_SOURCE +#endif /* _XOPEN_SOURCE */ + +#define _XOPEN_SOURCE 700 + +#include <assert.h> +#include <signal.h> +#include <stdbool.h> +#include <stdlib.h> +#include <stdint.h> +#include <stdio.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include <gtk/gtk.h> +#include <webkit/webkit.h> + +#define ARRLEN(arr) (sizeof (arr) / sizeof (arr)[0]) + +#define BROWSE_WINDOW_TITLE_MAX 256 +#define BROWSE_WINDOW_URL_MAX 1024 + +struct browse_window; +struct browse_window { + GtkWindow *window; + GtkEventController *event_handler; + + GdkClipboard *clipboard; + + WebKitWebView *webview; + + char title[BROWSE_WINDOW_TITLE_MAX]; + char url[BROWSE_WINDOW_URL_MAX]; + + struct browse_window *next, *prev; +}; + +struct browse_ctx { + GtkSettings *gtk_settings; + WebKitSettings *webkit_settings; + + struct browse_window *root; + + bool shutdown; +}; + +struct browse_gtk_setting { + char const *name; + union { gboolean b; guint u; gchar const *s; } v; +}; + +struct browse_webkit_setting { + char const *name; + union { gboolean b; guint u; gchar const *s; } v; +}; + +union browse_keybind_arg { + int i; + char const *s; +}; + +typedef void (*browse_keybind_fn)(struct browse_window *ctx, union browse_keybind_arg const *arg); + +struct browse_keybind { + GdkModifierType mod; + guint key; + browse_keybind_fn handler; + union browse_keybind_arg arg; +}; + +extern void +stopload(struct browse_window *ctx, union browse_keybind_arg const *arg); + +extern void +reload(struct browse_window *ctx, union browse_keybind_arg const *arg); + +extern void +navigate(struct browse_window *ctx, union browse_keybind_arg const *arg); + +extern void +clipboard(struct browse_window *ctx, union browse_keybind_arg const *arg); + +extern void +javascript(struct browse_window *ctx, union browse_keybind_arg const *arg); + +extern void +search(struct browse_window *ctx, union browse_keybind_arg const *arg); + +#endif /* BROWSE_H */ diff --git a/config.def.h b/config.def.h @@ -0,0 +1,46 @@ +#include "browse.h" + +static const char start_page[] = "https://searx.mblenczewski.com"; +static const char search_page[] = "https://searx.mblenczewski.com/search?q=%s"; + +// https://docs.gtk.org/gtk4/class/.Settings.html#Properties +static const struct browse_gtk_setting gtk_settings[] = { + { .name = "gtk-enable-animations", .v = { false }, }, + { .name = "gtk-application-prefer-dark-theme", .v = { true }, }, +}; + +// https://webkitgtk.org/reference/webkit2gtk/stable/class.Settings.html#Properties +static const struct browse_webkit_setting webkit_settings[] = { + { .name = "allow-file-access-from-file-urls", .v = { true }, }, + { .name = "enable-developer-extras", .v = { true }, }, + { .name = "enable-webgl", .v = { true }, }, + { .name = "enable-smooth-scrolling", .v = { true }, }, +}; + +#define BOOKMARK_FILE "$XDG_CONFIG_HOME/bookmarks" + +#define SEARCH_PROC { .s = "cat " BOOKMARK_FILE " | bemenu -l 10 -p 'Search: '", } + +#define MODKEY GDK_CONTROL_MASK + +static const struct browse_keybind keybinds[] = { + /* modifier keyval handler argument */ + { 0, GDK_KEY_Escape, stopload, { 0 }, }, + { MODKEY, GDK_KEY_c, stopload, { 0 }, }, + + { MODKEY|GDK_SHIFT_MASK, GDK_KEY_R, reload, { .i = 1, }, }, + { MODKEY, GDK_KEY_r, reload, { .i = 0, }, }, + + { MODKEY|GDK_SHIFT_MASK, GDK_KEY_H, navigate, { .i = -1, }, }, + { MODKEY|GDK_SHIFT_MASK, GDK_KEY_L, navigate, { .i = +1, }, }, + + { MODKEY, GDK_KEY_h, javascript, { .s = "window.scrollBy(-100, 0);", }, }, + { MODKEY, GDK_KEY_j, javascript, { .s = "window.scrollBy(0, +100);", }, }, + { MODKEY, GDK_KEY_k, javascript, { .s = "window.scrollBy(0, -100);", }, }, + { MODKEY, GDK_KEY_l, javascript, { .s = "window.scrollBy(+100, 0);", }, }, + + { MODKEY, GDK_KEY_y, clipboard, { .i = 0, }, }, + { MODKEY, GDK_KEY_p, clipboard, { .i = 1, }, }, + + { MODKEY, GDK_KEY_g, search, SEARCH_PROC, }, +}; diff --git a/config.mk b/config.mk @@ -0,0 +1,12 @@ +PREFIX ?= /usr/local + +OBJ := obj + +WEBKIT_INCS := $(shell pkg-config --cflags webkitgtk-6.0 webkitgtk-web-process-extension-6.0) +WEBKIT_LIBS := $(shell pkg-config --libs webkitgtk-6.0 webkitgtk-web-process-extension-6.0) + +WARNINGS := -Wall -Wextra -Wpedantic -Werror -Wno-extra-semi + +CFLAGS := -std=c17 $(WARNINGS) -Og -g +CPPFLAGS := $(WEBKIT_INCS) +LDFLAGS := $(WEBKIT_LIBS)