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.
@@ -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
@@ -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 = "..."