surf

surf
git clone git@git.zachrice.app:repos/surf.git
Log | Files | Refs | README | LICENSE

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:
A.gitignore | 7+++++++
AADBLOCK.md | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mconfig.def.h | 22+++++++++++++++++++---
Apatches/surf-adblock-webkit.diff | 117+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apatches/surf-bookmarks.diff | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Apatches/surf-prompt-on-open.diff | 31+++++++++++++++++++++++++++++++
Apatches/surf-zoom-keys.diff | 18++++++++++++++++++
Asurf-adblock-update | 33+++++++++++++++++++++++++++++++++
Msurf.c | 68+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
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();