commit dfb1c7703e55902671d1533d166115f46233b2c6
parent 48517e586cdc98bc1af7115674b554cc70c8bc2e
Author: Zach Rice <bynxmusic@gmail.com>
Date: Mon, 18 May 2026 17:46:41 -0400
Adblock, bookmarks, prompt for url on open, and enabled devtools with ctrl+shift+o
Diffstat:
9 files changed, 436 insertions(+), 8 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,7 @@
+surf
+*.o
+*.so
+config.h
+*.orig
+*.rej
+*.tar.gz
diff --git a/ADBLOCK.md b/ADBLOCK.md
@@ -0,0 +1,98 @@
+# surf adblock
+
+Ad/tracker/malware blocking for surf using WebKit's Content Filter API.
+
+## How it works
+
+`surf-adblock-update` downloads a hosts-based blocklist (Steven Black's
+unified list, ~80k domains), converts it to WebKit Content Blocker JSON,
+and saves it to `~/.surf/adblock.json`. On startup, surf compiles this
+JSON to bytecode and applies it to all page loads at the network layer.
+
+## Setup
+
+### 1. Install surf with the adblock patch applied
+
+ cd ~/suckless/surf
+ sudo make clean install
+
+This also installs `surf-adblock-update` if you add it to the Makefile
+install target, or you can run it directly from the repo:
+
+ ./surf-adblock-update
+
+### 2. Run the initial blocklist download
+
+ surf-adblock-update
+
+This creates `~/.surf/adblock.json`. You should see output like:
+
+ Downloading blocklist...
+ Done: 82207 domains blocked -> ~/.surf/adblock.json
+
+### 3. Set up automatic updates with cron
+
+Install cronie if you don't have it (Arch):
+
+ sudo pacman -S cronie
+ sudo systemctl enable --now cronie
+
+Add a weekly cron job (runs Sunday 4am):
+
+ crontab -e
+
+Add this line:
+
+ 0 4 * * 0 /home/YOUR_USER/suckless/surf/surf-adblock-update
+
+Or from the repo directory:
+
+ (crontab -l 2>/dev/null; echo "0 4 * * 0 $PWD/surf-adblock-update") | crontab -
+
+Verify it was added:
+
+ crontab -l
+
+### 4. Launch surf
+
+ GDK_BACKEND=x11 surf
+
+The filter loads asynchronously on startup. If there's an error
+compiling the filter, it prints to stderr.
+
+## Verifying it works
+
+Visit a known ad domain directly — it should fail to load:
+
+ GDK_BACKEND=x11 surf https://ads.google.com
+
+## Updating the blocklist
+
+Run `surf-adblock-update` any time, then restart surf to pick up changes.
+
+## Custom rules
+
+You can edit `~/.surf/adblock.json` directly. The format is WebKit
+Content Blocker JSON (same as Safari content blockers). Example rules:
+
+Block a specific domain:
+
+ {"trigger":{"url-filter":"^https?://([^/]*\\.)?example\\.com"},"action":{"type":"block"}}
+
+Hide page elements with CSS:
+
+ {"trigger":{"url-filter":".*"},"action":{"type":"css-display-none","selector":".ad-banner, .sponsored"}}
+
+Block cookies from third parties:
+
+ {"trigger":{"url-filter":".*","load-type":["third-party"]},"action":{"type":"block-cookies"}}
+
+Whitelist a site (cancel all previous rules):
+
+ {"trigger":{"url-filter":".*","if-domain":["trusted-site.com"]},"action":{"type":"ignore-previous-rules"}}
+
+## Disabling adblock
+
+Remove or rename the JSON file and restart surf:
+
+ mv ~/.surf/adblock.json ~/.surf/adblock.json.bak
diff --git a/config.def.h b/config.def.h
@@ -6,6 +6,8 @@ static char *styledir = "~/.surf/styles/";
static char *certdir = "~/.surf/certificates/";
static char *cachedir = "~/.surf/cache/";
static char *cookiefile = "~/.surf/cookies.txt";
+static char *adblockdir = "~/.surf/adblock/";
+static char *adblockfile = "~/.surf/adblock.json";
/* Webkit default features */
/* Highest priority value will be used.
@@ -29,7 +31,7 @@ static Parameter defconfig[ParameterLast] = {
[FontSize] = { { .i = 12 }, },
[Geolocation] = { { .i = 0 }, },
[HideBackground] = { { .i = 0 }, },
- [Inspector] = { { .i = 0 }, },
+ [Inspector] = { { .i = 1 }, },
[JavaScript] = { { .i = 1 }, },
[KioskMode] = { { .i = 0 }, },
[LoadImages] = { { .i = 1 }, },
@@ -69,8 +71,8 @@ static WebKitFindOptions findopts = WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE |
.v = (const char *[]){ "/bin/sh", "-c", \
"prop=\"$(printf '%b' \"$(xprop -id $1 "r" " \
"| sed -e 's/^"r"(UTF8_STRING) = \"\\(.*\\)\"/\\1/' " \
- " -e 's/\\\\\\(.\\)/\\1/g')\" " \
- "| dmenu -p '"p"' -w $1)\" " \
+ " -e 's/\\\\\\(.\\)/\\1/g' && cat ~/.surf/bookmarks)\" " \
+ "| dmenu -l 10 -p '"p"' -w $1)\" " \
"&& xprop -id $1 -f "s" 8u -set "s" \"$prop\"", \
"surf-setprop", winid, NULL \
} \
@@ -102,6 +104,17 @@ static WebKitFindOptions findopts = WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE |
} \
}
+/* BM_ADD(readprop) */
+#define BM_ADD(r) {\
+ .v = (const char *[]){ "/bin/sh", "-c", \
+ "(echo $(xprop -id $0 "r") | cut -d '\"' -f2 " \
+ "| sed 's/.*https*:\\/\\/\\(www\\.\\)\\?//' && cat ~/.surf/bookmarks) " \
+ "| awk '!seen[$0]++' > ~/.surf/bookmarks.tmp && " \
+ "mv ~/.surf/bookmarks.tmp ~/.surf/bookmarks", \
+ winid, NULL \
+ } \
+}
+
/* styles */
/*
* The iteration will stop at the first match, beginning at the beginning of
@@ -134,6 +147,8 @@ static Key keys[] = {
{ MODKEY, GDK_KEY_f, spawn, SETPROP("_SURF_FIND", "_SURF_FIND", PROMPT_FIND) },
{ MODKEY, GDK_KEY_slash, spawn, SETPROP("_SURF_FIND", "_SURF_FIND", PROMPT_FIND) },
+ { MODKEY, GDK_KEY_m, spawn, BM_ADD("_SURF_URI") },
+
{ 0, GDK_KEY_Escape, stop, { 0 } },
{ MODKEY, GDK_KEY_c, stop, { 0 } },
@@ -157,6 +172,7 @@ static Key keys[] = {
{ MODKEY|GDK_SHIFT_MASK, GDK_KEY_q, zoom, { .i = 0 } },
{ MODKEY, GDK_KEY_minus, zoom, { .i = -1 } },
{ MODKEY, GDK_KEY_plus, zoom, { .i = +1 } },
+ { MODKEY, GDK_KEY_equal, zoom, { .i = +1 } },
{ MODKEY, GDK_KEY_p, clipboard, { .i = 1 } },
{ MODKEY, GDK_KEY_y, clipboard, { .i = 0 } },
diff --git a/patches/surf-adblock-webkit.diff b/patches/surf-adblock-webkit.diff
@@ -0,0 +1,117 @@
+Description: WebKit Content Filter ad blocker for surf
+Uses WebKit2GTK's built-in Content Filter API (same engine as Safari
+content blockers) to block ads, trackers, and malware domains.
+
+On startup, surf compiles ~/.surf/adblock.json (WebKit Content Blocker
+JSON format) into bytecode and applies it to all views. The filter
+supports URL blocking, cookie blocking, and CSS element hiding.
+
+Run surf-adblock-update to download and convert Steven Black's unified
+hosts blocklist (~130k domains). Cron it weekly for auto-updates.
+
+Requires: curl (for the update script)
+Setup: surf-adblock-update && restart surf
+
+Apply: patch -p1 < patches/surf-adblock-webkit.diff
+Unpatch: patch -R -p1 < patches/surf-adblock-webkit.diff
+
+diff --git a/config.def.h b/config.def.h
+--- a/config.def.h
++++ b/config.def.h
+@@ -8,6 +8,8 @@
+ static char *cookiefile = "~/.surf/cookies.txt";
++static char *adblockdir = "~/.surf/adblock/";
++static char *adblockfile = "~/.surf/adblock.json";
+
+ /* Webkit default features */
+diff --git a/surf.c b/surf.c
+--- a/surf.c
++++ b/surf.c
+@@ -181,6 +181,10 @@
+ static void cleanup(void);
+
++/* Adblock */
++static void loadadblock(void);
++static void adblockloadcb(GObject *src, GAsyncResult *res, gpointer unused);
++
+ /* GTK/WebKit */
+@@ -260,6 +260,8 @@
+ static int spair[2];
++static WebKitUserContentFilterStore *filterstore;
++static WebKitUserContentFilter *adblockfilter;
+ char *argv0;
+@@ -360,6 +360,8 @@
+ cookiefile = buildfile(cookiefile);
+ scriptfile = buildfile(scriptfile);
+ certdir = buildpath(certdir);
++ adblockdir = buildpath(adblockdir);
++ adblockfile = buildfile(adblockfile);
+ if (curconfig[Ephemeral].val.i)
+@@ -429,6 +429,8 @@
+ }
++
++ loadadblock();
+ }
+@@ -1091,6 +1091,39 @@
+ }
+
++void
++loadadblock(void)
++{
++ GFile *gf;
++
++ if (!g_file_test(adblockfile, G_FILE_TEST_EXISTS))
++ return;
++
++ filterstore = webkit_user_content_filter_store_new(adblockdir);
++ gf = g_file_new_for_path(adblockfile);
++ webkit_user_content_filter_store_save_from_file(filterstore, "adblock",
++ gf, NULL, adblockloadcb, NULL);
++ g_object_unref(gf);
++}
++
++void
++adblockloadcb(GObject *src, GAsyncResult *res, gpointer unused)
++{
++ GError *err = NULL;
++ Client *c;
++
++ adblockfilter = webkit_user_content_filter_store_save_finish(
++ WEBKIT_USER_CONTENT_FILTER_STORE(src), res, &err);
++ if (err) {
++ fprintf(stderr, "surf: adblock: %s\n", err->message);
++ g_error_free(err);
++ return;
++ }
++ for (c = clients; c; c = c->next)
++ webkit_user_content_manager_add_filter(
++ webkit_web_view_get_user_content_manager(c->view),
++ adblockfilter);
++}
++
+ void
+ cleanup(void)
+ {
+@@ -1098,6 +1098,10 @@
+ while (clients)
+ destroyclient(clients);
+
++ if (adblockfilter)
++ webkit_user_content_filter_unref(adblockfilter);
++ if (filterstore)
++ g_object_unref(filterstore);
+ close(spair[0]);
+@@ -1103,6 +1103,8 @@
+ g_free(stylefile);
+ g_free(cachedir);
++ g_free(adblockdir);
++ g_free(adblockfile);
+ XCloseDisplay(dpy);
+@@ -1143,6 +1143,9 @@
+
+ contentmanager = webkit_user_content_manager_new();
++ if (adblockfilter)
++ webkit_user_content_manager_add_filter(
++ contentmanager, adblockfilter);
+
+ if (curconfig[Ephemeral].val.i) {
diff --git a/patches/surf-bookmarks.diff b/patches/surf-bookmarks.diff
@@ -0,0 +1,50 @@
+Description: Bookmarks support via dmenu and ~/.surf/bookmarks
+Ported from surf-bookmarks-20170722-723ff26.diff to current surf-webkit2.
+Ctrl+G (Go) and Ctrl+F (Find) prompts show bookmarks in dmenu.
+Ctrl+M adds current URL to ~/.surf/bookmarks (deduped, stripped of scheme).
+
+Requires: touch ~/.surf/bookmarks
+
+Apply: patch -p1 < patches/surf-bookmarks.diff
+Unpatch: patch -R -p1 < patches/surf-bookmarks.diff
+
+diff --git a/config.def.h b/config.def.h
+--- a/config.def.h
++++ b/config.def.h
+@@ -69,8 +69,8 @@
+ #define SETPROP(r, s, p) { \
+ .v = (const char *[]){ "/bin/sh", "-c", \
+ "prop=\"$(printf '%b' \"$(xprop -id $1 "r" " \
+ "| sed -e 's/^"r"(UTF8_STRING) = \"\\(.*\\)\"/\\1/' " \
+- " -e 's/\\\\\\(.\\)/\\1/g')\" " \
+- "| dmenu -p '"p"' -w $1)\" " \
++ " -e 's/\\\\\\(.\\)/\\1/g' && cat ~/.surf/bookmarks)\" " \
++ "| dmenu -l 10 -p '"p"' -w $1)\" " \
+ "&& xprop -id $1 -f "s" 8u -set "s" \"$prop\"", \
+ "surf-setprop", winid, NULL \
+ } \
+@@ -103,6 +103,17 @@
+ } \
+ }
+
++/* BM_ADD(readprop) */
++#define BM_ADD(r) {\
++ .v = (const char *[]){ "/bin/sh", "-c", \
++ "(echo $(xprop -id $0 "r") | cut -d '\"' -f2 " \
++ "| sed 's/.*https*:\\/\\/\\(www\\.\\)\\?//' && cat ~/.surf/bookmarks) " \
++ "| awk '!seen[$0]++' > ~/.surf/bookmarks.tmp && " \
++ "mv ~/.surf/bookmarks.tmp ~/.surf/bookmarks", \
++ winid, NULL \
++ } \
++}
++
+ /* styles */
+ /*
+ * The iteration will stop at the first match, beginning at the beginning of
+@@ -135,6 +146,7 @@
+ { MODKEY, GDK_KEY_f, spawn, SETPROP("_SURF_FIND", "_SURF_FIND", PROMPT_FIND) },
+ { MODKEY, GDK_KEY_slash, spawn, SETPROP("_SURF_FIND", "_SURF_FIND", PROMPT_FIND) },
+
++ { MODKEY, GDK_KEY_m, spawn, BM_ADD("_SURF_URI") },
+ { 0, GDK_KEY_Escape, stop, { 0 } },
+ { MODKEY, GDK_KEY_c, stop, { 0 } },
diff --git a/patches/surf-prompt-on-open.diff b/patches/surf-prompt-on-open.diff
@@ -0,0 +1,31 @@
+Description: Open dmenu URL prompt when surf is launched without a URI
+Instead of loading about:blank, spawns the Ctrl+G "Go:" prompt so you
+can immediately type a URL.
+
+Apply: patch -p1 < patches/surf-prompt-on-open.diff
+Unpatch: patch -R -p1 < patches/surf-prompt-on-open.diff
+
+diff --git a/surf.c b/surf.c
+--- a/surf.c
++++ b/surf.c
+@@ -2122,12 +2122,17 @@
+ if (argc > 0)
+ arg.v = argv[0];
+ else
+- arg.v = "about:blank";
++ arg.v = NULL;
+
+ setup();
+ c = newclient(NULL);
+ showview(NULL, c);
+
+- loaduri(c, &arg);
++ if (arg.v) {
++ loaduri(c, &arg);
++ } else {
++ arg = (Arg)SETPROP("_SURF_URI", "_SURF_GO", PROMPT_GO);
++ spawn(c, &arg);
++ }
+ updatetitle(c);
+
+ gtk_main();
diff --git a/patches/surf-zoom-keys.diff b/patches/surf-zoom-keys.diff
@@ -0,0 +1,18 @@
+Description: Add Ctrl+= as zoom-in shortcut (no Shift needed)
+The default config only has Ctrl++ which requires Shift on most keyboards.
+This adds Ctrl+= so zooming in works without Shift, matching standard browsers.
+
+Apply: patch -p1 < patches/surf-zoom-keys.diff
+Unpatch: patch -R -p1 < patches/surf-zoom-keys.diff
+
+diff --git a/config.def.h b/config.def.h
+--- a/config.def.h
++++ b/config.def.h
+@@ -157,6 +157,7 @@
+ { MODKEY|GDK_SHIFT_MASK, GDK_KEY_q, zoom, { .i = 0 } },
+ { MODKEY, GDK_KEY_minus, zoom, { .i = -1 } },
+ { MODKEY, GDK_KEY_plus, zoom, { .i = +1 } },
++ { MODKEY, GDK_KEY_equal, zoom, { .i = +1 } },
+
+ { MODKEY, GDK_KEY_p, clipboard, { .i = 1 } },
+ { MODKEY, GDK_KEY_y, clipboard, { .i = 0 } },
diff --git a/surf-adblock-update b/surf-adblock-update
@@ -0,0 +1,33 @@
+#!/bin/sh
+# surf-adblock-update - download hosts blocklists and convert to WebKit JSON
+#
+# Fetches Steven Black's unified hosts (ads + malware + trackers),
+# converts to WebKit Content Blocker JSON, saves to ~/.surf/adblock.json.
+# Restart surf after running to pick up changes.
+#
+# Usage: surf-adblock-update
+# crontab: 0 4 * * 0 surf-adblock-update
+
+OUTFILE="${HOME}/.surf/adblock.json"
+HOSTS_URL="https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"
+
+mkdir -p "${HOME}/.surf"
+
+echo "Downloading blocklist..."
+curl -sL "$HOSTS_URL" \
+ | grep '^0\.0\.0\.0 ' \
+ | awk '{print $2}' \
+ | grep -v '^0\.0\.0\.0$' \
+ | sort -u \
+ | awk '
+BEGIN { printf "[" }
+{
+ gsub(/\./, "\\\\.", $0)
+ if (NR > 1) printf ","
+ printf "{\"trigger\":{\"url-filter\":\"^https?://([^/]*\\\\.)?%s\"},\"action\":{\"type\":\"block\"}}", $0
+}
+END { printf "]\n" }
+' > "${OUTFILE}.tmp" && mv "${OUTFILE}.tmp" "$OUTFILE"
+
+domains=$(grep -o '"block"' "$OUTFILE" | wc -l)
+echo "Done: ${domains} domains blocked -> ${OUTFILE}"
diff --git a/surf.c b/surf.c
@@ -181,6 +181,10 @@ static void msgext(Client *c, char type, const Arg *a);
static void destroyclient(Client *c);
static void cleanup(void);
+/* Adblock */
+static void loadadblock(void);
+static void adblockloadcb(GObject *src, GAsyncResult *res, gpointer unused);
+
/* GTK/WebKit */
static WebKitWebView *newview(Client *c, WebKitWebView *rv);
static void initwebextensions(WebKitWebContext *wc, Client *c);
@@ -258,6 +262,8 @@ static const char *useragent;
static Parameter *curconfig;
static int modparams[ParameterLast];
static int spair[2];
+static WebKitUserContentFilterStore *filterstore;
+static WebKitUserContentFilter *adblockfilter;
char *argv0;
static ParamName loadtransient[] = {
@@ -352,9 +358,11 @@ setup(void)
curconfig = defconfig;
/* dirs and files */
- cookiefile = buildfile(cookiefile);
- scriptfile = buildfile(scriptfile);
- certdir = buildpath(certdir);
+ cookiefile = buildfile(cookiefile);
+ scriptfile = buildfile(scriptfile);
+ certdir = buildpath(certdir);
+ adblockdir = buildpath(adblockdir);
+ adblockfile = buildfile(adblockfile);
if (curconfig[Ephemeral].val.i)
cachedir = NULL;
else
@@ -418,6 +426,8 @@ setup(void)
uriparams[i].config[j] = defconfig[j];
}
}
+
+ loadadblock();
}
void
@@ -1083,17 +1093,57 @@ destroyclient(Client *c)
}
void
+loadadblock(void)
+{
+ GFile *gf;
+
+ if (!g_file_test(adblockfile, G_FILE_TEST_EXISTS))
+ return;
+
+ filterstore = webkit_user_content_filter_store_new(adblockdir);
+ gf = g_file_new_for_path(adblockfile);
+ webkit_user_content_filter_store_save_from_file(filterstore, "adblock",
+ gf, NULL, adblockloadcb, NULL);
+ g_object_unref(gf);
+}
+
+void
+adblockloadcb(GObject *src, GAsyncResult *res, gpointer unused)
+{
+ GError *err = NULL;
+ Client *c;
+
+ adblockfilter = webkit_user_content_filter_store_save_finish(
+ WEBKIT_USER_CONTENT_FILTER_STORE(src), res, &err);
+ if (err) {
+ fprintf(stderr, "surf: adblock: %s\n", err->message);
+ g_error_free(err);
+ return;
+ }
+ for (c = clients; c; c = c->next)
+ webkit_user_content_manager_add_filter(
+ webkit_web_view_get_user_content_manager(c->view),
+ adblockfilter);
+}
+
+void
cleanup(void)
{
while (clients)
destroyclient(clients);
+ if (adblockfilter)
+ webkit_user_content_filter_unref(adblockfilter);
+ if (filterstore)
+ g_object_unref(filterstore);
close(spair[0]);
close(spair[1]);
g_free(cookiefile);
g_free(scriptfile);
g_free(stylefile);
g_free(cachedir);
+ g_free(adblockdir);
+ g_free(adblockfile);
XCloseDisplay(dpy);
}
@@ -1141,6 +1191,9 @@ newview(Client *c, WebKitWebView *rv)
useragent = webkit_settings_get_user_agent(settings);
contentmanager = webkit_user_content_manager_new();
+ if (adblockfilter)
+ webkit_user_content_manager_add_filter(
+ contentmanager, adblockfilter);
if (curconfig[Ephemeral].val.i) {
context = webkit_web_context_new_ephemeral();
@@ -2120,13 +2173,18 @@ main(int argc, char *argv[])
if (argc > 0)
arg.v = argv[0];
else
- arg.v = "about:blank";
+ arg.v = NULL;
setup();
c = newclient(NULL);
showview(NULL, c);
- loaduri(c, &arg);
+ if (arg.v) {
+ loaduri(c, &arg);
+ } else {
+ arg = (Arg)SETPROP("_SURF_URI", "_SURF_GO", PROMPT_GO);
+ spawn(c, &arg);
+ }
updatetitle(c);
gtk_main();