Organizational tabs with OpenResty
Motivation
I have a problem. I hoard browser tabs. In multiple windows. So much so that I need TabCloud to have an overview and do the occasional reorganization. Here's an illustration with some details redacted:
This doesn't look that bad, because
- a) I've been cutting back lately
- b) this is not the whole thing
- c) this is just one browser, I use multiple ones for headspace separation and privacy reasons
- d) the organization is already in place
I'm talking about the black icons at the start of each row. Now, there's the option to name the windows, but that has a fatal weakness: it doesn't persist across browser restarts. There's an option to save windows, but that requires having an account and logging in, which I won't do, again, for privacy reasons.
To get around this, I started making these small favicons and serving them on a subdomain. Looks something like this:
Pin the tab, and get a nice visual indicator in both the window and TabCloud. This is achieved by a bit of nginx config and a small HTML page:
server {
server_name org.aldum.pw;
listen 80;
override_charset on;
charset utf-8;
access_log off;
error_log off;
location / {
root /srv/http/aldum.pw/org;
if ($request_uri ~ ^/(.*)\.html$) {
return 302 /$1;
}
try_files $uri $uri.html $uri/ =404;
}
}
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta http-equiv="X-Clacks-Overhead" content="GNU Terry Pratchett" />
<link rel="stylesheet" type="text/css" href="style.css">
<link rel="shortcut icon" href="out/@=NAME=@.png" />
<title>@=NAME=@</title>
</head>
<body>
<center>
<img src="out/@=NAME=@.png" />
</center>
</body>
</html>
This is the template, of course, I relatively quickly got tired of manually
adding new ones, hence the @=NAME=@
and a quick'n'dirty shell script was added.
#!/bin/sh
[ -n "$1" ] && {
name="$1"
nf="$name".html
cp -ap temp.html "$nf"
sed -i -s "s/@=NAME=@/$name/g" "$nf"
} || echo 'error'
This still needs to be invoked manually, which is kinda' dumb. Computers are our friends, especially when it comes to repetitive, unimaginative tasks. Lately, I had to make a couple of new ones, and in addition to assembling and saving the icon, has another step of running the script. I now have about 30. I figured it's time to automate, at least this simple part.
I've always been running OpenResty, which boils down to nginx + lua. Never really took advantage of it's capabilities, until now! Lua is great, because it's small (both in grammar and interpreter size), and thanks to JIT compiling, performant even. This makes it great for embedding in other software for scripting and extendability.
Notable features/oddities include:
- 1-based indexing of arrays
- accidental globals
- tables (which arrays actually are)
- metatables (this allows some OO-like behavior)
- unambigous grammar without semicolons, braces, or signifcant whitespace
- first-class functions
But enough about lua, let's get to work.
Setup
According to the documentation, we can provide an HTML response
with content_by_lua*
directives.
location / {
root /srv/http/aldum.pw/org;
content_by_lua_file "org.lua";
}
ngx.say("<p>hello, world</p>")
Hmm, I'm getting a 404. What gives?
Turning on the error log, apparently relative paths are not looked up starting at the webroot, but relative to nginx's path.
2023/02/12 18:06:32 [error] 9492#0: *939454 failed to load external Lua file "/opt/openresty/nginx/org.lua": cannot open /opt/openresty/nginx/org.lua: No such file or directory, client: [...], server: test.aldum.pw, request: "GET /book HTTP/1.1", host: "test.aldum.pw"
Weird, but easy enough to fix, let's just provide a full path.
But there's a new problem: it's downloading, not displaying a site.
The content is correct though: <p>hello, world</p>
.
Taking a closer look at the tutorial, a default_type
should be
provided:
location / {
root /srv/http/aldum.pw/org;
default_type text/html;
content_by_lua_file "/etc/openresty/org.lua";
}
Okay, looking good. Concept proven, now we can go deeper:
local page = 'book'
local content = [[
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta http-equiv="X-Clacks-Overhead" content="GNU Terry Pratchett" />
<link rel="stylesheet" type="text/css" href="style.css">
<link rel="shortcut icon" href="out/{page}.png" />
<title>{page}</title>
</head>
<body>
<center>
<img src="out/{page}.png" />
</center>
</body>
</html>
]]
ngx.say(
string.gsub(content, "{page}", page)
)
The CSS is AWOL. Nothing we can't fix with a bit of nginx magic.
As a bonus, I won't need to copy style.css
around from now on.
location = /style.css {
root /srv/http/aldum.pw;
}
Glancing a little further down, what fresh hell is this? Did I leave a stray '3' somewhere?
I did nost. But I am changing 3 occurences with that gsub
, maybe it's also
returning that?
Yes, yes it is.
The
string.gsub
function also returns as a second result the number of times it made the substitution. For instance, an easy way to count the number of spaces in a string is_, count = string.gsub(str, " ", " ")
(Remember, the _ is just a dummy variable name.)
See, this is why we do static type checking, it should have been caught that a tuple is being returned to where a string is expected... I will be exploring typed lua in the future.
Adding an intermediate variable and ignoring the count fixes the problem:
local render, _ = string.gsub(content, "{page}", page)
ngx.say(render)
That's better, but it needs one more tweak in nginx, because now it's resolving pictures the same way as the pages, and that's obviously not going to work.
So let's add one more location
block above /
, like so:
location = /style.css [...]
location /out/ {
root /srv/http/aldum.pw/org;
}
location / [...]
Okay, getting there. What's missing? Observant readers may have noticed, that I deferred a step.
Right, it's a fixed string literal, not observing the request URL at all. Fixing that:
local page = string.gsub(ngx.var.uri, '/', '', 1)
ngx.var.uri
contains the nginx $uri
value, and I'm just stripping the
leading slash, because it looks wrong as the title.
So that's it, we have a working solution for the HTML side of things. For the next trick, I'll think of a way to automatically generate the icons as well.
Full configs
server {
server_name org.aldum.pw;
listen 80;
override_charset on;
charset utf-8;
access_log off;
error_log off;
location = /style.css {
root /srv/http/aldum.pw;
}
location /out/ {
root /srv/http/aldum.pw/org;
}
location / {
root /srv/http/aldum.pw/org;
default_type text/html;
content_by_lua_file "/etc/openresty/org.lua";
}
}
local page = string.gsub(ngx.var.uri, '/', '', 1)
local content = [[
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta http-equiv="X-Clacks-Overhead" content="GNU Terry Pratchett" />
<link rel="stylesheet" type="text/css" href="style.css">
<link rel="shortcut icon" href="out/{page}.png" />
<title>{page}</title>
</head>
<body>
<center>
<img src="out/{page}.png" />
</center>
</body>
</html>
]]
local render, _ = string.gsub(content, "{page}", page)
ngx.say(render)