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 | ++++++++ |
A | LICENSE | | | 21 | +++++++++++++++++++++ |
A | Makefile | | | 50 | ++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | README | | | 3 | +++ |
A | browse.c | | | 281 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | browse.h | | | 94 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | config.def.h | | | 46 | ++++++++++++++++++++++++++++++++++++++++++++++ |
A | config.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)