code.pablomurad.com

skull-browser

Keyboard-driven browser for the small web, with native Gopher and Gemini

feat: adblock funcional, CI, about redesenhada, e remocao de API morta

adblock_bootstrap baixa e mantem as listas de filtro. O modulo adblock
lia .txt de um diretorio e nada mais: sem capacidade de buscar lista
alguma, uma instalacao nova bloqueava exatamente nada. Agora baixa
EasyList, EasyPrivacy e a lista em portugues via curl, valida o conteudo
antes de aceitar (uma pagina de erro HTML seria parseada como regras sem
reclamar) e recarrega. O modulo nao faz nada ao ser carregado: quem
dispara e o rc.lua, o que mantem a suite de testes fora da rede.

Corrigido no adblock.lua: load transmitia update_rules mas nao o estado
de cada lista, entao processo web existente ficava com a lista carregada
e desligada ate alguem alternar na mao.

CI em GitHub Actions: dependencias, build e testes a cada push e PR.

process_limit removido por inteiro. O codigo que aplicava o valor estava
sob #if !WEBKIT_CHECK_VERSION(2,26,0), morto ha anos, enquanto a API
seguia exposta ao Lua aceitando valores. O proprio rc.lua definia
process_limit = 4, com um comentario acima admitindo que nao tinha
efeito.

docs/decisoes/001 registra por que ficamos no webkit2gtk-4.1 e a regra de
nao crescer extension/.

skull://about redesenhada em ingles: nome, versao, developed by pmurad,
link para pablomurad.com, e a mencao ao luakit sob GPLv3.

Titulo da janela volta a ser texto puro. O emoji so renderiza se o proprio
browser desenhar a barra de titulo; com GTK_WINDOW_TOPLEVEL sem titlebar
quem desenha e o compositor, que no WSLg roda em outro conteiner com as
fontes dele.
author
Pablo Murad
date
commit
6c47fac45fe3a5de87939fd18bd630a4567a1915
parent
23c8ac88fb1042b47ff08c855444e69a618e4d39
changes
12 files +310 −63
@@ -0,0 +1,25 @@+name: build
+
+on:
+  push:
+  pull_request:
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          # getversion.sh deriva a versao de `git describe --tags`; sem o
+          # historico completo o build reporta versao errada.
+          fetch-depth: 0
+
+      - name: Dependencias
+        run: sh build-utils/setup-deps.sh
+
+      - name: Build
+        run: make -j$(nproc)
+
+      - name: Testes
+        run: make run-tests
@@ -747,8 +747,6 @@ /* push boolean properties */       PB_CASE(VERBOSE,          log_get_verbosity("all") >= LOG_LEVEL_verbose)
       PB_CASE(NOUNIQUE,         globalconf.nounique)
       PB_CASE(ENABLE_SPELL_CHECKING,    webkit_web_context_get_spell_checking_enabled(web_context_get()))
-      /* push integer properties */
-      PI_CASE(PROCESS_LIMIT,    web_context_process_limit_get())
       case L_TK_OPTIONS:
         return luaH_luakit_push_options_table(L);
       case L_TK_WEBSITE_DATA:
@@ -821,10 +819,6 @@ return 0;     luakit_token_t token = l_tokenize(lua_tostring(L, 2));
 
     switch (token) {
-        case L_TK_PROCESS_LIMIT:
-            if (!web_context_process_limit_set(lua_tointeger(L, 3)))
-                return luaL_error(L, "Too late to set WebKit process limit");
-            break;
         case L_TK_ENABLE_SPELL_CHECKING:
             webkit_web_context_set_spell_checking_enabled(web_context_get(),
                     luaH_checkboolean(L, 3));
@@ -244,7 +244,6 @@ certificate allow_certificate
 options
 crash
-process_limit
 wrap_js
 editable
 is_playing_audio
@@ -26,7 +26,6 @@ require "unique_instance" 
 -- Set the number of web processes to use. A value of 0 means 'no limit'. This
 -- has no effect since WebKit 2.26
-luakit.process_limit = 4
 -- Set the cookie storage location
 soup.cookies_storage = luakit.data_dir .. "/cookies.db"
 
@@ -79,6 +78,8 @@ -- Add adblock
 local adblock = require "adblock"
 local adblock_chrome = require "adblock_chrome"
+local adblock_bootstrap = require "adblock_bootstrap"
+adblock_bootstrap.update_when_idle()
 
 local webinspector = require "webinspector"
 
@@ -0,0 +1,70 @@+# 001 — Ficar no webkit2gtk-4.1
+
+**Data:** 2026-09-19
+**Situação:** aceita
+
+## O problema
+
+O Skull Browser compila contra `webkit2gtk-4.1`, a API do WebKitGTK para GTK 3.
+A versão atual da API upstream é `webkitgtk-6.0`, que exige GTK 4 e libsoup 3.
+
+Duas frases da documentação oficial definem o risco.
+
+Do guia de migração para a 6.0:
+
+> All APIs that were previously deprecated in webkit2gtk-4.0 and webkit2gtk-4.1
+> have been removed. [...] It also includes the entire GObject DOM API (e.g.
+> `WebKitDOMDocument`), which has been **removed without replacement**. Use
+> JavaScript to interact with and manipulate the DOM instead.
+
+E, no mesmo documento:
+
+> Beware that as of WebKitGTK 2.40, **the entire web process API may
+> unfortunately be removed in the future.**
+
+## Por que isso importa aqui
+
+A segunda frase é a séria. A arquitetura do luakit — e portanto a nossa — é
+construída em cima exatamente dessa API:
+
+- `extension/` são ~1.900 linhas de C que existem para rodar uma VM Lua
+  **dentro** do processo de renderização do WebKit
+- os 10 módulos `lib/*_wm.lua` (~1.480 linhas) assumem acesso síncrono ao DOM
+  a partir desse processo
+- `extension/scroll.c` chega a definir `WEBKIT_DOM_USE_UNSTABLE_API` para obter
+  a posição de rolagem
+
+Se a API de processo web for removida, isso não é ajuste: cada propriedade
+síncrona de `dom_element` vira uma ida e volta assíncrona, e os 10 módulos
+precisam ser repensados.
+
+Migrar para a 6.0 também arrasta GTK 3 → GTK 4, que é um porte independente e
+maior que qualquer fase já planejada neste projeto.
+
+## Decisão
+
+**Ficar no `webkit2gtk-4.1`.** Não migrar agora.
+
+A pressão real é baixa: a 4.1 é suportada upstream, o Ubuntu 24.04 entrega
+WebKitGTK 2.52.6 com ela, e o build passa com avisos de depreciação apenas —
+nenhum erro. Não há prazo anunciado para a remoção.
+
+## Consequência operacional
+
+**Não adicionar código novo em `extension/`.**
+
+Código escrito ali é código com prazo de validade. Quando uma funcionalidade
+puder ser feita no processo da interface, ou via `eval_js`, esse é o caminho —
+mesmo que pareça mais desajeitado hoje.
+
+## Quando revisar
+
+- Quando uma distro que nos interessa parar de empacotar `webkit2gtk-4.1`
+- Quando o upstream anunciar data para remover a API de processo web
+- Se surgir necessidade que force crescer `extension/` — aí o custo da migração
+  precisa ser comparado com o custo de continuar
+
+## Fontes
+
+- WebKit 6.0: Migrating WebKitGTK Applications to GTK 4 (documentação oficial)
+- WebKitGTK API Versions Demystified — Michael Catanzaro, 2025-04-28
@@ -18,47 +18,84 @@ --- CSS applied to the about page. -- @type string
 -- @readwrite
 _M.stylesheet = [===[
+    :root {
+        --bg:      #07090c;
+        --phos:    #3df07a;
+        --phos-dim:#1f8f49;
+        --bone:    #e9e5d9;
+        --muted:   #5d6470;
+        --cyan:    #35d6c3;
+    }
+    * { box-sizing: border-box; }
     body {
         display: flex;
         align-items: center;
         justify-content: center;
-        min-height: 78vh;
+        min-height: 92vh;
         margin: 0;
-        background: #0c0d10;
-        color: #e7e3d8;
         font-family: monospace;
+        color: var(--bone);
+        background:
+            radial-gradient(ellipse at 50% 28%, #10281b 0%, transparent 62%),
+            var(--bg);
     }
-    #about {
-        text-align: center;
-        line-height: 1.7;
+    /* Varredura de tubo: sutil, so para o fundo nao ficar morto. */
+    body::after {
+        content: "";
+        position: fixed;
+        inset: 0;
+        pointer-events: none;
+        background: repeating-linear-gradient(
+            to bottom, rgba(0,0,0,0.22) 0 1px, transparent 1px 3px);
     }
+    #about { text-align: center; line-height: 1.75; }
     #mark {
-        width: 96px;
-        height: 96px;
-        margin: 0 auto 26px;
+        width: 104px;
+        height: 104px;
+        margin: 0 auto 30px;
         image-rendering: pixelated;
+        filter: drop-shadow(0 0 14px rgba(61,240,122,0.45));
     }
     #name {
-        font-size: 30px;
+        font-size: 44px;
         font-weight: 700;
+        letter-spacing: 12px;
+        margin: 0 0 2px;
+        padding-left: 12px;
+        color: var(--phos);
+        text-shadow: 0 0 18px rgba(61,240,122,0.55);
+    }
+    #tagline {
+        font-size: 12px;
         letter-spacing: 3px;
-        margin: 0;
-        color: #3df07a;
+        text-transform: uppercase;
+        color: var(--phos-dim);
+        margin: 0 0 26px;
     }
     #version {
-        font-size: 13px;
-        color: #6f7480;
-        margin: 6px 0 30px;
+        font-size: 12px;
+        color: var(--muted);
+        margin: 0 0 32px;
     }
-    #author {
-        font-size: 15px;
-        margin: 0 0 30px;
+    #author { font-size: 15px; margin: 0 0 6px; }
+    #author b { color: var(--phos); font-weight: 700; }
+    #site {
+        display: inline-block;
+        font-size: 13px;
+        color: var(--cyan);
+        text-decoration: none;
+        border-bottom: 1px solid rgba(53,214,195,0.35);
+        padding-bottom: 1px;
+        margin-bottom: 34px;
     }
+    #site:hover { border-bottom-color: var(--cyan); }
     #origin {
-        font-size: 12px;
-        color: #6f7480;
-        max-width: 46ex;
+        font-size: 11px;
+        color: var(--muted);
+        max-width: 52ex;
         margin: 0 auto;
+        padding-top: 20px;
+        border-top: 1px solid rgba(93,100,112,0.22);
     }
 ]===]
 
@@ -86,15 +123,17 @@ local html_template = [==[ <!DOCTYPE html>
 <html>
 <head>
-    <title>Sobre</title>
+    <title>About</title>
     <style>{stylesheet}</style>
 </head>
 <body>
     <div id="about">
         <div id="mark">{mark}</div>
-        <p id="name">SKULL BROWSER</p>
+        <p id="name">SKULL</p>
+        <p id="tagline">{tagline}</p>
         <p id="version">{version}</p>
-        <p id="author">{author}</p>
+        <p id="author">developed by <b>{author}</b></p>
+        <a id="site" href="{site}">{site}</a>
         <p id="origin">{origin}</p>
     </div>
 </body>
@@ -105,19 +144,21 @@ chrome.add("about", function ()     local subs = {
         stylesheet = _M.stylesheet,
         mark = mark_svg,
+        tagline = "keyboard-driven browser for the whole web",
         version = lousy.util.escape(luakit.version),
-        author = "Pablo Murad",
-        origin = "Baseado no luakit. Distribuido sob a GNU GPLv3.",
+        author = "pmurad",
+        site = "https://pablomurad.com",
+        origin = "Based on luakit. Distributed under the GNU GPLv3.",
     }
     return (string.gsub(html_template, "{(%w+)}", subs))
 end)
 
 add_binds("normal", {
-    { "^gA$", "Abrir a pagina sobre.", function (w) w:navigate("skull://about/") end },
+    { "^gA$", "Open the about page.", function (w) w:navigate("skull://about/") end },
 })
 
 add_cmds({
-    { ":about", "Abrir a pagina sobre.", function (w) w:navigate("skull://about/") end },
+    { ":about", "Open the about page.", function (w) w:navigate("skull://about/") end },
 })
 
 return _M
@@ -431,6 +431,13 @@ end 
     if not no_sync and not single_list then
         adblock_wm:emit_signal("update_rules", _M.rules)
+        -- update_rules troca a tabela de regras mas nao mexe em enabled_rules,
+        -- entao um processo web existente ficava com a lista carregada e
+        -- desligada ate alguem alternar na mao. Reemitir o estado resolve.
+        for name, list in pairs(_M.rules) do
+            adblock_wm:emit_signal("list_set_enabled", name,
+                util.table.hasitem(list.opts or {}, "Enabled") and true or false)
+        end
     end
     _M.refresh_views()
 end
@@ -0,0 +1,135 @@+--- Keep the adblock filter lists on disk.
+--
+-- The adblock module reads `*.txt` files from its data directory and does
+-- nothing else: it has no way to fetch a list, so a fresh install blocks
+-- nothing at all. This module downloads the default lists on first run and
+-- refreshes them once they go stale.
+--
+-- Downloads go through `curl` rather than the browser's own download stack,
+-- which would show every refresh in the download list as if the user had asked
+-- for it.
+--
+-- @module adblock_bootstrap
+-- @copyright 2026 Pablo Murad <pablomurad@pm.me>
+
+local lfs = require("lfs")
+local adblock = require("adblock")
+local modes = require("modes")
+
+local _M = {}
+
+--- Lists fetched by default.
+-- Each entry needs a `file` ending in `.txt`, which is the only extension the
+-- adblock module looks at.
+-- @type table
+-- @readwrite
+_M.lists = {
+    { file = "easylist.txt",
+      uri = "https://easylist.to/easylist/easylist.txt" },
+    { file = "easyprivacy.txt",
+      uri = "https://easylist.to/easylist/easyprivacy.txt" },
+    { file = "easylistportuguese.txt",
+      uri = "https://easylist-downloads.adblockplus.org/easylistportuguese.txt" },
+}
+
+--- Days before a list on disk is considered stale.
+-- @type number
+-- @readwrite
+_M.max_age_days = 7
+
+local dir = luakit.data_dir .. "/adblock/"
+
+local function age_days(path)
+    local mtime = lfs.attributes(path, "modification")
+    if not mtime then return nil end
+    return (os.time() - mtime) / 86400
+end
+
+-- A truncated download or a captive-portal login page would otherwise be
+-- parsed as filter rules without a word of complaint.
+local function looks_like_filterlist(path)
+    local f = io.open(path, "rb")
+    if not f then return false end
+    local head = f:read(256) or ""
+    f:close()
+    return head:match("^%[Adblock") ~= nil or head:match("^!") ~= nil
+end
+
+local pending = 0
+
+local function finish()
+    pending = pending - 1
+    if pending > 0 then return end
+
+    adblock.load(true)
+    msg.info("adblock: listas recarregadas")
+end
+
+local function fetch(entry)
+    local dest, part = dir .. entry.file, dir .. entry.file .. ".part"
+    local cmd = string.format("curl -fsSL --max-time 120 -o %q %q", part, entry.uri)
+
+    local ok, err = pcall(luakit.spawn, cmd, function (_, status)
+        if status == 0 and looks_like_filterlist(part) then
+            os.remove(dest)
+            os.rename(part, dest)
+            msg.info("adblock: %s atualizada", entry.file)
+        else
+            os.remove(part)
+            msg.warn("adblock: falha ao baixar %s (status %s)", entry.file, tostring(status))
+        end
+        finish()
+    end)
+
+    if not ok then
+        msg.warn("adblock: nao consegui executar curl: %s", tostring(err))
+        finish()
+    end
+end
+
+--- Download any list that is missing or stale.
+-- @tparam[opt] boolean force Refresh every list regardless of age.
+_M.update = function (force)
+    if not lfs.attributes(dir, "mode") then lfs.mkdir(dir) end
+
+    local todo = {}
+    for _, entry in ipairs(_M.lists) do
+        local age = age_days(dir .. entry.file)
+        if force or not age or age > _M.max_age_days then
+            todo[#todo + 1] = entry
+        end
+    end
+
+    if #todo == 0 then
+        msg.verbose("adblock: listas em dia")
+        return 0
+    end
+
+    pending = #todo
+    for _, entry in ipairs(todo) do fetch(entry) end
+    return #todo
+end
+
+modes.add_cmds({
+    { ":adblock-update", "Baixar ou atualizar as listas de filtro.",
+        function (w)
+            local n = _M.update(true)
+            w:notify(string.format("adblock: baixando %d lista(s)...", n))
+        end },
+})
+
+--- Schedule an update off the startup path.
+-- `adblock.load` parses synchronously on the UI thread and EasyList alone is
+-- over two megabytes, so this defers to idle. Call it from the config; the
+-- module deliberately does nothing on require, which keeps the test suite off
+-- the network.
+_M.update_when_idle = function ()
+    luakit.idle_add(function ()
+        _M.update(false)
+        return false
+    end)
+end
+
+return _M
+
+-- vim: et:sw=4:ts=8:sts=4:tw=80
@@ -416,7 +416,7 @@ end, 
     update_win_title = function (w)
         local uri, title = w.view.uri, w.view.title
-        title = (title or "Skull Browser") .. ((uri and " - " .. uri) or "")
+        title = (title or "Skull") .. ((uri and " - " .. uri) or "")
         local max = settings.get_setting("window.max_title_len")
         if utf8.len(title) > max then
             local suffix = "..."
@@ -26,8 +26,6 @@ #include <webkit2/webkit2.h> 
 /** WebKit context common to all web views */
 static WebKitWebContext *web_context;
-/** WebKit process count; default to unlimited */
-static guint process_limit = 0;
 /** Whether the web context startup function has been run */
 static gboolean web_context_started = FALSE;
 
@@ -39,21 +37,6 @@ web_context_get(void) {
     g_assert(web_context);
     return web_context;
-}
-
-guint
-web_context_process_limit_get(void)
-{
-    return process_limit;
-}
-
-gboolean
-web_context_process_limit_set(guint limit)
-{
-    if (web_context_started)
-        return FALSE;
-    process_limit = limit;
-    return TRUE;
 }
 
 static void
@@ -104,12 +87,6 @@ web_context_init_finish(void) {
     if (web_context_started)
         return;
-
-#if !WEBKIT_CHECK_VERSION(2,26,0)
-    webkit_web_context_set_process_model(web_context, WEBKIT_PROCESS_MODEL_MULTIPLE_SECONDARY_PROCESSES);
-    info("Web process count: %d", process_limit);
-    webkit_web_context_set_web_process_count_limit(web_context, process_limit);
-#endif
 
     web_context_started = TRUE;
 }
@@ -27,8 +27,6 @@ void web_context_init(void); void web_context_init_finish(void);
 WebKitWebContext *web_context_get(void);
 WebKitWebContext *web_context_get_private(void);
-guint web_context_process_limit_get(void);
-gboolean web_context_process_limit_set(guint limit);
 
 #endif
 
@@ -227,7 +227,7 @@ /* create and setup window widget */     w->widget = gtk_window_new(GTK_WINDOW_TOPLEVEL);
     d->win = GTK_WINDOW(w->widget);
     gtk_window_set_default_size(d->win, 800, 600);
-    gtk_window_set_title(d->win, "Skull Browser");
+    gtk_window_set_title(d->win, "Skull");
     if (globalconf.application)
         gtk_window_set_application(d->win, globalconf.application);